Skip to content

Commit 87a44c3

Browse files
authored
System.IO files cleanup (#61413)
* Environment.SystemPageSize returns cached value * we are no longer shipping MS.IO.Redist, so we can use Array.MaxLength directly * we are no longer shipping MS.IO.Redist, there is no need for File to be partial * we are no longer shipping MS.IO.Redist, there is no need for FileInfo to be partial * there is no need for .Win32.cs and .Windows.cs file anymore
1 parent 35704e4 commit 87a44c3

File tree

8 files changed

+272
-322
lines changed

8 files changed

+272
-322
lines changed

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,9 @@
414414
<Compile Include="$(MSBuildThisFileDirectory)System\IO\EnumerationOptions.cs" />
415415
<Compile Include="$(MSBuildThisFileDirectory)System\IO\EndOfStreamException.cs" />
416416
<Compile Include="$(MSBuildThisFileDirectory)System\IO\File.cs" />
417-
<Compile Include="$(MSBuildThisFileDirectory)System\IO\File.netcoreapp.cs" />
418417
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileAccess.cs" />
419418
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileAttributes.cs" />
420419
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileInfo.cs" />
421-
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileInfo.netcoreapp.cs" />
422420
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileLoadException.cs" />
423421
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileMode.cs" />
424422
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileNotFoundException.cs" />
@@ -1840,7 +1838,6 @@
18401838
<Compile Include="$(MSBuildThisFileDirectory)System\Guid.Windows.cs" />
18411839
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DisableMediaInsertionPrompt.cs" />
18421840
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DriveInfoInternal.Windows.cs" />
1843-
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystem.Win32.cs" />
18441841
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystem.Windows.cs" />
18451842
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystemInfo.Windows.cs" />
18461843
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.Windows.cs" />

src/libraries/System.Private.CoreLib/src/System/IO/File.cs

Lines changed: 219 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ namespace System.IO
1616
{
1717
// Class for creating FileStream objects, and some basic file management
1818
// routines such as Delete, etc.
19-
public static partial class File
19+
public static class File
2020
{
21-
// Don't use Array.MaxLength. MS.IO.Redist targets .NET Framework.
22-
private const int MaxByteArrayLength = 0x7FFFFFC7;
21+
private const int ChunkSize = 8192;
2322
private static Encoding? s_UTF8NoBOM;
2423

2524
// UTF-8 without BOM and with error detection. Same as the default encoding for StreamWriter.
@@ -121,6 +120,12 @@ public static bool Exists([NotNullWhen(true)] string? path)
121120
return false;
122121
}
123122

123+
/// <summary>
124+
/// Initializes a new instance of the <see cref="FileStream" /> class with the specified path, creation mode, read/write and sharing permission, the access other FileStreams can have to the same file, the buffer size, additional file options and the allocation size.
125+
/// </summary>
126+
/// <remarks><see cref="FileStream(string,System.IO.FileStreamOptions)"/> for information about exceptions.</remarks>
127+
public static FileStream Open(string path, FileStreamOptions options) => new FileStream(path, options);
128+
124129
public static FileStream Open(string path, FileMode mode)
125130
=> Open(path, mode, (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), FileShare.None);
126131

@@ -130,6 +135,44 @@ public static FileStream Open(string path, FileMode mode, FileAccess access)
130135
public static FileStream Open(string path, FileMode mode, FileAccess access, FileShare share)
131136
=> new FileStream(path, mode, access, share);
132137

138+
/// <summary>
139+
/// Initializes a new instance of the <see cref="Microsoft.Win32.SafeHandles.SafeFileHandle" /> class with the specified path, creation mode, read/write and sharing permission, the access other SafeFileHandles can have to the same file, additional file options and the allocation size.
140+
/// </summary>
141+
/// <param name="path">A relative or absolute path for the file that the current <see cref="Microsoft.Win32.SafeHandles.SafeFileHandle" /> instance will encapsulate.</param>
142+
/// <param name="mode">One of the enumeration values that determines how to open or create the file. The default value is <see cref="FileMode.Open" /></param>
143+
/// <param name="access">A bitwise combination of the enumeration values that determines how the file can be accessed. The default value is <see cref="FileAccess.Read" /></param>
144+
/// <param name="share">A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is <see cref="FileShare.Read" />.</param>
145+
/// <param name="preallocationSize">The initial allocation size in bytes for the file. A positive value is effective only when a regular file is being created, overwritten, or replaced.
146+
/// Negative values are not allowed. In other cases (including the default 0 value), it's ignored.</param>
147+
/// <param name="options">An object that describes optional <see cref="Microsoft.Win32.SafeHandles.SafeFileHandle" /> parameters to use.</param>
148+
/// <exception cref="T:System.ArgumentNullException"><paramref name="path" /> is <see langword="null" />.</exception>
149+
/// <exception cref="T:System.ArgumentException"><paramref name="path" /> is an empty string (""), contains only white space, or contains one or more invalid characters.
150+
/// -or-
151+
/// <paramref name="path" /> refers to a non-file device, such as <c>CON:</c>, <c>COM1:</c>, <c>LPT1:</c>, etc. in an NTFS environment.</exception>
152+
/// <exception cref="T:System.NotSupportedException"><paramref name="path" /> refers to a non-file device, such as <c>CON:</c>, <c>COM1:</c>, <c>LPT1:</c>, etc. in a non-NTFS environment.</exception>
153+
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="preallocationSize" /> is negative.
154+
/// -or-
155+
/// <paramref name="mode" />, <paramref name="access" />, or <paramref name="share" /> contain an invalid value.</exception>
156+
/// <exception cref="T:System.IO.FileNotFoundException">The file cannot be found, such as when <paramref name="mode" /> is <see cref="FileMode.Truncate" /> or <see cref="FileMode.Open" />, and the file specified by <paramref name="path" /> does not exist. The file must already exist in these modes.</exception>
157+
/// <exception cref="T:System.IO.IOException">An I/O error, such as specifying <see cref="FileMode.CreateNew" /> when the file specified by <paramref name="path" /> already exists, occurred.
158+
/// -or-
159+
/// The disk was full (when <paramref name="preallocationSize" /> was provided and <paramref name="path" /> was pointing to a regular file).
160+
/// -or-
161+
/// The file was too large (when <paramref name="preallocationSize" /> was provided and <paramref name="path" /> was pointing to a regular file).</exception>
162+
/// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission.</exception>
163+
/// <exception cref="T:System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception>
164+
/// <exception cref="T:System.UnauthorizedAccessException">The <paramref name="access" /> requested is not permitted by the operating system for the specified <paramref name="path" />, such as when <paramref name="access" /> is <see cref="FileAccess.Write" /> or <see cref="FileAccess.ReadWrite" /> and the file or directory is set for read-only access.
165+
/// -or-
166+
/// <see cref="F:System.IO.FileOptions.Encrypted" /> is specified for <paramref name="options" />, but file encryption is not supported on the current platform.</exception>
167+
/// <exception cref="T:System.IO.PathTooLongException">The specified path, file name, or both exceed the system-defined maximum length. </exception>
168+
public static SafeFileHandle OpenHandle(string path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read,
169+
FileShare share = FileShare.Read, FileOptions options = FileOptions.None, long preallocationSize = 0)
170+
{
171+
Strategies.FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize: 0, options, preallocationSize);
172+
173+
return SafeFileHandle.Open(Path.GetFullPath(path), mode, access, share, options, preallocationSize);
174+
}
175+
133176
// File and Directory UTC APIs treat a DateTimeKind.Unspecified as UTC whereas
134177
// ToUniversalTime treats this as local.
135178
internal static DateTimeOffset GetUtcDateTimeOffset(DateTime dateTime)
@@ -537,9 +580,9 @@ private static async Task<byte[]> InternalReadAllBytesUnknownLengthAsync(FileStr
537580
if (bytesRead == rentedArray.Length)
538581
{
539582
uint newLength = (uint)rentedArray.Length * 2;
540-
if (newLength > MaxByteArrayLength)
583+
if (newLength > Array.MaxLength)
541584
{
542-
newLength = (uint)Math.Max(MaxByteArrayLength, rentedArray.Length + 1);
585+
newLength = (uint)Math.Max(Array.MaxLength, rentedArray.Length + 1);
543586
}
544587

545588
byte[] tmp = ArrayPool<byte>.Shared.Rent((int)newLength);
@@ -731,5 +774,176 @@ private static void Validate(string path, Encoding encoding)
731774
if (path.Length == 0)
732775
throw new ArgumentException(SR.Argument_EmptyPath, nameof(path));
733776
}
777+
778+
private static byte[] ReadAllBytesUnknownLength(FileStream fs)
779+
{
780+
byte[]? rentedArray = null;
781+
Span<byte> buffer = stackalloc byte[512];
782+
try
783+
{
784+
int bytesRead = 0;
785+
while (true)
786+
{
787+
if (bytesRead == buffer.Length)
788+
{
789+
uint newLength = (uint)buffer.Length * 2;
790+
if (newLength > Array.MaxLength)
791+
{
792+
newLength = (uint)Math.Max(Array.MaxLength, buffer.Length + 1);
793+
}
794+
795+
byte[] tmp = ArrayPool<byte>.Shared.Rent((int)newLength);
796+
buffer.CopyTo(tmp);
797+
byte[]? oldRentedArray = rentedArray;
798+
buffer = rentedArray = tmp;
799+
if (oldRentedArray != null)
800+
{
801+
ArrayPool<byte>.Shared.Return(oldRentedArray);
802+
}
803+
}
804+
805+
Debug.Assert(bytesRead < buffer.Length);
806+
int n = fs.Read(buffer.Slice(bytesRead));
807+
if (n == 0)
808+
{
809+
return buffer.Slice(0, bytesRead).ToArray();
810+
}
811+
bytesRead += n;
812+
}
813+
}
814+
finally
815+
{
816+
if (rentedArray != null)
817+
{
818+
ArrayPool<byte>.Shared.Return(rentedArray);
819+
}
820+
}
821+
}
822+
823+
private static void WriteToFile(string path, FileMode mode, string? contents, Encoding encoding)
824+
{
825+
ReadOnlySpan<byte> preamble = encoding.GetPreamble();
826+
int preambleSize = preamble.Length;
827+
828+
using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.None, GetPreallocationSize(mode, contents, encoding, preambleSize));
829+
long fileOffset = mode == FileMode.Append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0;
830+
831+
if (string.IsNullOrEmpty(contents))
832+
{
833+
if (preambleSize > 0 // even if the content is empty, we want to store the preamble
834+
&& fileOffset == 0) // if we're appending to a file that already has data, don't write the preamble.
835+
{
836+
RandomAccess.WriteAtOffset(fileHandle, preamble, fileOffset);
837+
}
838+
return;
839+
}
840+
841+
int bytesNeeded = preambleSize + encoding.GetMaxByteCount(Math.Min(contents.Length, ChunkSize));
842+
byte[]? rentedBytes = null;
843+
Span<byte> bytes = bytesNeeded <= 1024 ? stackalloc byte[1024] : (rentedBytes = ArrayPool<byte>.Shared.Rent(bytesNeeded));
844+
845+
try
846+
{
847+
if (fileOffset == 0)
848+
{
849+
preamble.CopyTo(bytes);
850+
}
851+
else
852+
{
853+
preambleSize = 0; // don't append preamble to a non-empty file
854+
}
855+
856+
Encoder encoder = encoding.GetEncoder();
857+
ReadOnlySpan<char> remaining = contents;
858+
while (!remaining.IsEmpty)
859+
{
860+
ReadOnlySpan<char> toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize));
861+
remaining = remaining.Slice(toEncode.Length);
862+
int encoded = encoder.GetBytes(toEncode, bytes.Slice(preambleSize), flush: remaining.IsEmpty);
863+
Span<byte> toStore = bytes.Slice(0, preambleSize + encoded);
864+
865+
RandomAccess.WriteAtOffset(fileHandle, toStore, fileOffset);
866+
867+
fileOffset += toStore.Length;
868+
preambleSize = 0;
869+
}
870+
}
871+
finally
872+
{
873+
if (rentedBytes is not null)
874+
{
875+
ArrayPool<byte>.Shared.Return(rentedBytes);
876+
}
877+
}
878+
}
879+
880+
private static async Task WriteToFileAsync(string path, FileMode mode, string? contents, Encoding encoding, CancellationToken cancellationToken)
881+
{
882+
ReadOnlyMemory<byte> preamble = encoding.GetPreamble();
883+
int preambleSize = preamble.Length;
884+
885+
using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, GetPreallocationSize(mode, contents, encoding, preambleSize));
886+
long fileOffset = mode == FileMode.Append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0;
887+
888+
if (string.IsNullOrEmpty(contents))
889+
{
890+
if (preambleSize > 0 // even if the content is empty, we want to store the preamble
891+
&& fileOffset == 0) // if we're appending to a file that already has data, don't write the preamble.
892+
{
893+
await RandomAccess.WriteAtOffsetAsync(fileHandle, preamble, fileOffset, cancellationToken).ConfigureAwait(false);
894+
}
895+
return;
896+
}
897+
898+
byte[] bytes = ArrayPool<byte>.Shared.Rent(preambleSize + encoding.GetMaxByteCount(Math.Min(contents.Length, ChunkSize)));
899+
900+
try
901+
{
902+
if (fileOffset == 0)
903+
{
904+
preamble.CopyTo(bytes);
905+
}
906+
else
907+
{
908+
preambleSize = 0; // don't append preamble to a non-empty file
909+
}
910+
911+
Encoder encoder = encoding.GetEncoder();
912+
ReadOnlyMemory<char> remaining = contents.AsMemory();
913+
while (!remaining.IsEmpty)
914+
{
915+
ReadOnlyMemory<char> toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize));
916+
remaining = remaining.Slice(toEncode.Length);
917+
int encoded = encoder.GetBytes(toEncode.Span, bytes.AsSpan(preambleSize), flush: remaining.IsEmpty);
918+
ReadOnlyMemory<byte> toStore = new ReadOnlyMemory<byte>(bytes, 0, preambleSize + encoded);
919+
920+
await RandomAccess.WriteAtOffsetAsync(fileHandle, toStore, fileOffset, cancellationToken).ConfigureAwait(false);
921+
922+
fileOffset += toStore.Length;
923+
preambleSize = 0;
924+
}
925+
}
926+
finally
927+
{
928+
ArrayPool<byte>.Shared.Return(bytes);
929+
}
930+
}
931+
932+
private static long GetPreallocationSize(FileMode mode, string? contents, Encoding encoding, int preambleSize)
933+
{
934+
// for a single write operation, setting preallocationSize has no perf benefit, as it requires an additional sys-call
935+
if (contents is null || contents.Length < ChunkSize)
936+
{
937+
return 0;
938+
}
939+
940+
// preallocationSize is ignored for Append mode, there is no need to spend cycles on GetByteCount
941+
if (mode == FileMode.Append)
942+
{
943+
return 0;
944+
}
945+
946+
return preambleSize + encoding.GetByteCount(contents);
947+
}
734948
}
735949
}

0 commit comments

Comments
 (0)