From a0bf8e47d153ee9a32e425f613f4bcbbb8083bca Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 24 May 2021 10:57:08 +0200 Subject: [PATCH 1/4] move File.Exists and File.ReadAllBytes to Common: remove code duplication and implementation differences --- src/libraries/Common/src/System/IO/File.cs | 140 ++++++++++++++++++ .../IO/FileSystem.Attributes.Windows.cs | 4 +- .../src/System/IO/FileSystem.Exists.Unix.cs | 4 + .../src/Microsoft.IO.Redist.csproj | 2 + .../src/System.IO.FileSystem.csproj | 5 +- .../src/System/IO/File.cs | 115 +------------- .../src/Internal/IO/File.Unix.cs | 24 --- .../src/Internal/IO/File.Windows.cs | 74 --------- .../src/Internal/IO/File.cs | 75 ---------- .../System.Private.CoreLib.Shared.projitems | 12 +- .../Runtime/Loader/AssemblyLoadContext.cs | 4 +- 11 files changed, 165 insertions(+), 294 deletions(-) create mode 100644 src/libraries/Common/src/System/IO/File.cs rename src/libraries/{System.IO.FileSystem => Common}/src/System/IO/FileSystem.Exists.Unix.cs (97%) delete mode 100644 src/libraries/System.Private.CoreLib/src/Internal/IO/File.Unix.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/Internal/IO/File.Windows.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs diff --git a/src/libraries/Common/src/System/IO/File.cs b/src/libraries/Common/src/System/IO/File.cs new file mode 100644 index 00000000000000..1ed02d53c6f3b6 --- /dev/null +++ b/src/libraries/Common/src/System/IO/File.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +#if SYSTEM_PRIVATE_CORELIB +namespace Internal.IO +#elif MS_IO_REDIST +namespace Microsoft.IO +#else +namespace System.IO +#endif +{ +#if SYSTEM_PRIVATE_CORELIB + internal +#else + public +#endif + static partial class File + { + // Tests if 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. + public static bool Exists([NotNullWhen(true)] string? path) + { + try + { + if (path == null) + return false; + if (path.Length == 0) + 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 + Debug.Assert(path != null, "File.Exists: GetFullPath returned null"); + if (path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1])) + { + return false; + } + + return FileSystem.FileExists(path); + } + catch (ArgumentException) { } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + + return false; + } + + public static byte[] ReadAllBytes(string path) + { + // bufferSize == 1 used to avoid unnecessary buffer in FileStream + using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) + { + long fileLength = fs.Length; + if (fileLength > int.MaxValue) + { + throw new IOException(SR.IO_FileTooLong2GB); + } + else if (fileLength == 0) + { +#if !MS_IO_REDIST + // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content. + // Thus we need to assume 0 doesn't mean empty. + return ReadAllBytesUnknownLength(fs); +#endif + } + + int index = 0; + int count = (int)fileLength; + byte[] bytes = new byte[count]; + while (count > 0) + { + int n = fs.Read(bytes, index, count); + if (n == 0) + ThrowEndOfFile(); + index += n; + count -= n; + } + return bytes; + } + + static void ThrowEndOfFile() => throw new EndOfStreamException(SR.IO_EOF_ReadBeyondEOF); + } + +#if !MS_IO_REDIST + private static byte[] ReadAllBytesUnknownLength(FileStream fs) + { + byte[]? rentedArray = null; + Span buffer = stackalloc byte[512]; + try + { + int bytesRead = 0; + while (true) + { + if (bytesRead == buffer.Length) + { + uint newLength = (uint)buffer.Length * 2; + if (newLength > Array.MaxLength) + { + newLength = (uint)Math.Max(Array.MaxLength, buffer.Length + 1); + } + + byte[] tmp = ArrayPool.Shared.Rent((int)newLength); + buffer.CopyTo(tmp); + if (rentedArray != null) + { + ArrayPool.Shared.Return(rentedArray); + } + buffer = rentedArray = tmp; + } + + Debug.Assert(bytesRead < buffer.Length); + int n = fs.Read(buffer.Slice(bytesRead)); + if (n == 0) + { + return buffer.Slice(0, bytesRead).ToArray(); + } + bytesRead += n; + } + } + finally + { + if (rentedArray != null) + { + ArrayPool.Shared.Return(rentedArray); + } + } + } +#endif + } +} diff --git a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs index 60e7a0aa466f1d..17d29743d2970d 100644 --- a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs @@ -9,7 +9,9 @@ using System.IO; using System.Text; -#if MS_IO_REDIST +#if SYSTEM_PRIVATE_CORELIB +namespace Internal.IO +#elif MS_IO_REDIST namespace Microsoft.IO #else namespace System.IO diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/FileSystem.Exists.Unix.cs b/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs similarity index 97% rename from src/libraries/System.IO.FileSystem/src/System/IO/FileSystem.Exists.Unix.cs rename to src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs index c0210d5b43bb4e..722a0402b8b355 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/FileSystem.Exists.Unix.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs @@ -3,7 +3,11 @@ using System.Diagnostics; +#if SYSTEM_PRIVATE_CORELIB +namespace Internal.IO +#else namespace System.IO +#endif { /// Provides an implementation of FileSystem for Unix systems. internal static partial class FileSystem diff --git a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj index 94b9552748a987..6ab2252428783d 100644 --- a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj +++ b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj @@ -110,6 +110,8 @@ Link="Common\System\IO\DisableMediaInsertionPrompt.cs" /> + + + - diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/File.cs b/src/libraries/System.IO.FileSystem/src/System/IO/File.cs index 9f597548564731..97e23220c368cb 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/File.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/File.cs @@ -21,7 +21,7 @@ namespace System.IO { // Class for creating FileStream objects, and some basic file management // routines such as Delete, etc. - public static class File + public static partial class File { private const int MaxByteArrayLength = 0x7FFFFFC7; private static Encoding? s_UTF8NoBOM; @@ -111,39 +111,6 @@ public static void Delete(string path) FileSystem.DeleteFile(Path.GetFullPath(path)); } - // 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. - public static bool Exists([NotNullWhen(true)] string? path) - { - try - { - if (path == null) - return false; - if (path.Length == 0) - 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 - Debug.Assert(path != null, "File.Exists: GetFullPath returned null"); - if (path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1])) - { - return false; - } - - return FileSystem.FileExists(path); - } - catch (ArgumentException) { } - catch (IOException) { } - catch (UnauthorizedAccessException) { } - - return false; - } - public static FileStream Open(string path, FileMode mode) { return Open(path, mode, (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), FileShare.None); @@ -326,86 +293,6 @@ public static void WriteAllText(string path, string? contents, Encoding encoding } } - public static byte[] ReadAllBytes(string path) - { - // bufferSize == 1 used to avoid unnecessary buffer in FileStream - using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, FileOptions.SequentialScan)) - { - long fileLength = fs.Length; - if (fileLength > int.MaxValue) - { - throw new IOException(SR.IO_FileTooLong2GB); - } - else if (fileLength == 0) - { -#if !MS_IO_REDIST - // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content. - // Thus we need to assume 0 doesn't mean empty. - return ReadAllBytesUnknownLength(fs); -#endif - } - - int index = 0; - int count = (int)fileLength; - byte[] bytes = new byte[count]; - while (count > 0) - { - int n = fs.Read(bytes, index, count); - if (n == 0) - throw Error.GetEndOfFile(); - index += n; - count -= n; - } - return bytes; - } - } - -#if !MS_IO_REDIST - private static byte[] ReadAllBytesUnknownLength(FileStream fs) - { - byte[]? rentedArray = null; - Span buffer = stackalloc byte[512]; - try - { - int bytesRead = 0; - while (true) - { - if (bytesRead == buffer.Length) - { - uint newLength = (uint)buffer.Length * 2; - if (newLength > MaxByteArrayLength) - { - newLength = (uint)Math.Max(MaxByteArrayLength, buffer.Length + 1); - } - - byte[] tmp = ArrayPool.Shared.Rent((int)newLength); - buffer.CopyTo(tmp); - if (rentedArray != null) - { - ArrayPool.Shared.Return(rentedArray); - } - buffer = rentedArray = tmp; - } - - Debug.Assert(bytesRead < buffer.Length); - int n = fs.Read(buffer.Slice(bytesRead)); - if (n == 0) - { - return buffer.Slice(0, bytesRead).ToArray(); - } - bytesRead += n; - } - } - finally - { - if (rentedArray != null) - { - ArrayPool.Shared.Return(rentedArray); - } - } - } -#endif - public static void WriteAllBytes(string path, byte[] bytes) { if (path == null) diff --git a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.Unix.cs b/src/libraries/System.Private.CoreLib/src/Internal/IO/File.Unix.cs deleted file mode 100644 index 8cf6606246e4c3..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.Unix.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Internal.IO -{ - internal static partial class File - { - internal static bool InternalExists(string fullPath) - { - Interop.Sys.FileStatus fileinfo; - - // First use stat, as we want to follow symlinks. If that fails, it could be because the symlink - // is broken, we don't have permissions, etc., in which case fall back to using LStat to evaluate - // based on the symlink itself. - if (Interop.Sys.Stat(fullPath, out fileinfo) < 0 && - Interop.Sys.LStat(fullPath, out fileinfo) < 0) - { - return false; - } - - return ((fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFDIR); - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.Windows.cs b/src/libraries/System.Private.CoreLib/src/Internal/IO/File.Windows.cs deleted file mode 100644 index 354384be17ec0f..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.Windows.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Win32.SafeHandles; -using System.IO; -using System.Runtime.InteropServices; - -namespace Internal.IO -{ - internal static partial class File - { - internal static bool InternalExists(string fullPath) - { - Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; - int errorCode = FillAttributeInfo(fullPath, ref data, returnErrorOnNotFound: true); - - return (errorCode == 0) && (data.dwFileAttributes != -1) - && ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) == 0); - } - - /// - /// Returns 0 on success, otherwise a Win32 error code. Note that - /// classes should use -1 as the uninitialized state for dataInitialized. - /// - internal static int FillAttributeInfo(string path, ref Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data, bool returnErrorOnNotFound) - { - int errorCode = Interop.Errors.ERROR_SUCCESS; - - using (DisableMediaInsertionPrompt.Create()) - { - if (!Interop.Kernel32.GetFileAttributesEx(path, Interop.Kernel32.GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, ref data)) - { - errorCode = Marshal.GetLastPInvokeError(); - if (errorCode == Interop.Errors.ERROR_ACCESS_DENIED) - { - // Files that are marked for deletion will not let you GetFileAttributes, - // ERROR_ACCESS_DENIED is given back without filling out the data struct. - // FindFirstFile, however, will. Historically we always gave back attributes - // for marked-for-deletion files. - - Interop.Kernel32.WIN32_FIND_DATA findData = default; - using (SafeFindHandle handle = Interop.Kernel32.FindFirstFile(path, ref findData)) - { - if (handle.IsInvalid) - { - errorCode = Marshal.GetLastPInvokeError(); - } - else - { - errorCode = Interop.Errors.ERROR_SUCCESS; - data.PopulateFrom(ref findData); - } - } - } - } - } - - if (errorCode != Interop.Errors.ERROR_SUCCESS && !returnErrorOnNotFound) - { - switch (errorCode) - { - case Interop.Errors.ERROR_FILE_NOT_FOUND: - case Interop.Errors.ERROR_PATH_NOT_FOUND: - case Interop.Errors.ERROR_NOT_READY: // Removable media not ready - // Return default value for backward compatibility - data.dwFileAttributes = -1; - return Interop.Errors.ERROR_SUCCESS; - } - } - - return errorCode; - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs b/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs deleted file mode 100644 index 127d72daa2e366..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/Internal/IO/File.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Security; -using System.IO; - -namespace Internal.IO -{ - // - // Subsetted clone of System.IO.File for internal runtime use. - // Keep in sync with https://github.com/dotnet/runtime/tree/main/src/libraries/System.IO.FileSystem. - // - internal static partial class File - { - // Tests if 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. - public static bool Exists([NotNullWhen(true)] string? path) - { - try - { - if (path == null) - return false; - if (path.Length == 0) - 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 - Debug.Assert(path != null, "File.Exists: GetFullPath returned null"); - if (path.Length > 0 && PathInternal.IsDirectorySeparator(path[^1])) - { - return false; - } - - return InternalExists(path); - } - catch (ArgumentException) { } - catch (IOException) { } - catch (UnauthorizedAccessException) { } - - return false; - } - - public static byte[] ReadAllBytes(string path) - { - // bufferSize == 1 used to avoid unnecessary buffer in FileStream - using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1)) - { - long fileLength = fs.Length; - if (fileLength > int.MaxValue) - throw new IOException(SR.IO_FileTooLong2GB); - - int index = 0; - int count = (int)fileLength; - byte[] bytes = new byte[count]; - while (count > 0) - { - int n = fs.Read(bytes, index, count); - if (n == 0) - ThrowHelper.ThrowEndOfFileException(); - index += n; - count -= n; - } - return bytes; - } - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 7e427704dcb9e0..11e5cae7f58c89 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -54,7 +54,6 @@ - @@ -1171,6 +1170,9 @@ Common\System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs + + Common\System\IO\File.cs + Common\System\Runtime\CompilerServices\IsExternalInit.cs @@ -1656,8 +1658,10 @@ Common\Interop\Windows\User32\Interop.USEROBJECTFLAGS.cs + + Common\System\IO\FileSystem.Attributes.Windows.cs + - @@ -1875,8 +1879,10 @@ Common\Interop\Unix\System.Native\Interop.Write.cs + + Common\System\IO\FileSystem.Exists.Unix.cs + - diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs index e990d5e4918696..9d60b44bee416a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @@ -780,7 +780,7 @@ private static void OnAssemblyLoad(RuntimeAssembly assembly) string assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!, $"{assemblyName.Name}.dll"); - bool exists = Internal.IO.File.InternalExists(assemblyPath); + bool exists = Internal.IO.FileSystem.FileExists(assemblyPath); if (!exists && Path.IsCaseSensitive) { #if CORECLR @@ -790,7 +790,7 @@ private static void OnAssemblyLoad(RuntimeAssembly assembly) } #endif // CORECLR assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!.ToLowerInvariant(), $"{assemblyName.Name}.dll"); - exists = Internal.IO.File.InternalExists(assemblyPath); + exists = Internal.IO.FileSystem.FileExists(assemblyPath); } Assembly? asm = exists ? parentALC.LoadFromAssemblyPath(assemblyPath) : null; From 567ad3413efb27a82b6b6b1ad4024ce9475bff38 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 24 May 2021 12:52:22 +0200 Subject: [PATCH 2/4] hopefully fix the Unix build --- src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs b/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs index 722a0402b8b355..e8160277853546 100644 --- a/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; #if SYSTEM_PRIVATE_CORELIB From c0e93c163e8447c55ae6e751c72f4d273f5d5ff7 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 24 May 2021 13:17:18 +0200 Subject: [PATCH 3/4] hopefully fix the Unix build 2/n --- src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs | 1 + .../src/System.Private.CoreLib.Shared.projitems | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs b/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs index e8160277853546..26f4ec6175b3a5 100644 --- a/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.Exists.Unix.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.IO; #if SYSTEM_PRIVATE_CORELIB namespace Internal.IO diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 11e5cae7f58c89..bb46002c3d21ae 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1867,6 +1867,9 @@ Common\Interop\Unix\System.Native\Interop.Stat.cs + + Common\Interop\Unix\System.Native\Interop.Stat.Span.cs + Common\Interop\Unix\System.Native\Interop.SysConf.cs From c0d32617086ef66776a8f2013432eeb8026c68fc Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 24 May 2021 13:17:18 +0200 Subject: [PATCH 4/4] hopefully fix the Unix build 3/n --- .../src/System.Private.CoreLib.Shared.projitems | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index bb46002c3d21ae..602191582a79c1 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1882,6 +1882,9 @@ Common\Interop\Unix\System.Native\Interop.Write.cs + Common\System\Text\ValueUtf8Converter.cs + Common\System\IO\FileSystem.Exists.Unix.cs