Skip to content

Commit

Permalink
System.IO files cleanup (#61413)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
adamsitnik committed Nov 12, 2021
1 parent 35704e4 commit 87a44c3
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 322 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -414,11 +414,9 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IO\EnumerationOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\EndOfStreamException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\File.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\File.netcoreapp.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileAccess.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileAttributes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileInfo.netcoreapp.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileLoadException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileMode.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileNotFoundException.cs" />
Expand Down Expand Up @@ -1840,7 +1838,6 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Guid.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DisableMediaInsertionPrompt.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DriveInfoInternal.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystem.Win32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystem.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystemInfo.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.Windows.cs" />
Expand Down
224 changes: 219 additions & 5 deletions src/libraries/System.Private.CoreLib/src/System/IO/File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ namespace System.IO
{
// Class for creating FileStream objects, and some basic file management
// routines such as Delete, etc.
public static partial class File
public static class File
{
// Don't use Array.MaxLength. MS.IO.Redist targets .NET Framework.
private const int MaxByteArrayLength = 0x7FFFFFC7;
private const int ChunkSize = 8192;
private static Encoding? s_UTF8NoBOM;

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

/// <summary>
/// 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.
/// </summary>
/// <remarks><see cref="FileStream(string,System.IO.FileStreamOptions)"/> for information about exceptions.</remarks>
public static FileStream Open(string path, FileStreamOptions options) => new FileStream(path, options);

public static FileStream Open(string path, FileMode mode)
=> Open(path, mode, (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), FileShare.None);

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

/// <summary>
/// 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.
/// </summary>
/// <param name="path">A relative or absolute path for the file that the current <see cref="Microsoft.Win32.SafeHandles.SafeFileHandle" /> instance will encapsulate.</param>
/// <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>
/// <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>
/// <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>
/// <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.
/// Negative values are not allowed. In other cases (including the default 0 value), it's ignored.</param>
/// <param name="options">An object that describes optional <see cref="Microsoft.Win32.SafeHandles.SafeFileHandle" /> parameters to use.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="path" /> is <see langword="null" />.</exception>
/// <exception cref="T:System.ArgumentException"><paramref name="path" /> is an empty string (""), contains only white space, or contains one or more invalid characters.
/// -or-
/// <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>
/// <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>
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="preallocationSize" /> is negative.
/// -or-
/// <paramref name="mode" />, <paramref name="access" />, or <paramref name="share" /> contain an invalid value.</exception>
/// <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>
/// <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.
/// -or-
/// The disk was full (when <paramref name="preallocationSize" /> was provided and <paramref name="path" /> was pointing to a regular file).
/// -or-
/// The file was too large (when <paramref name="preallocationSize" /> was provided and <paramref name="path" /> was pointing to a regular file).</exception>
/// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission.</exception>
/// <exception cref="T:System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception>
/// <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.
/// -or-
/// <see cref="F:System.IO.FileOptions.Encrypted" /> is specified for <paramref name="options" />, but file encryption is not supported on the current platform.</exception>
/// <exception cref="T:System.IO.PathTooLongException">The specified path, file name, or both exceed the system-defined maximum length. </exception>
public static SafeFileHandle OpenHandle(string path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read,
FileShare share = FileShare.Read, FileOptions options = FileOptions.None, long preallocationSize = 0)
{
Strategies.FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize: 0, options, preallocationSize);

return SafeFileHandle.Open(Path.GetFullPath(path), mode, access, share, options, preallocationSize);
}

// File and Directory UTC APIs treat a DateTimeKind.Unspecified as UTC whereas
// ToUniversalTime treats this as local.
internal static DateTimeOffset GetUtcDateTimeOffset(DateTime dateTime)
Expand Down Expand Up @@ -537,9 +580,9 @@ private static async Task<byte[]> InternalReadAllBytesUnknownLengthAsync(FileStr
if (bytesRead == rentedArray.Length)
{
uint newLength = (uint)rentedArray.Length * 2;
if (newLength > MaxByteArrayLength)
if (newLength > Array.MaxLength)
{
newLength = (uint)Math.Max(MaxByteArrayLength, rentedArray.Length + 1);
newLength = (uint)Math.Max(Array.MaxLength, rentedArray.Length + 1);
}

byte[] tmp = ArrayPool<byte>.Shared.Rent((int)newLength);
Expand Down Expand Up @@ -731,5 +774,176 @@ private static void Validate(string path, Encoding encoding)
if (path.Length == 0)
throw new ArgumentException(SR.Argument_EmptyPath, nameof(path));
}

private static byte[] ReadAllBytesUnknownLength(FileStream fs)
{
byte[]? rentedArray = null;
Span<byte> 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<byte>.Shared.Rent((int)newLength);
buffer.CopyTo(tmp);
byte[]? oldRentedArray = rentedArray;
buffer = rentedArray = tmp;
if (oldRentedArray != null)
{
ArrayPool<byte>.Shared.Return(oldRentedArray);
}
}

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<byte>.Shared.Return(rentedArray);
}
}
}

private static void WriteToFile(string path, FileMode mode, string? contents, Encoding encoding)
{
ReadOnlySpan<byte> preamble = encoding.GetPreamble();
int preambleSize = preamble.Length;

using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.None, GetPreallocationSize(mode, contents, encoding, preambleSize));
long fileOffset = mode == FileMode.Append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0;

if (string.IsNullOrEmpty(contents))
{
if (preambleSize > 0 // even if the content is empty, we want to store the preamble
&& fileOffset == 0) // if we're appending to a file that already has data, don't write the preamble.
{
RandomAccess.WriteAtOffset(fileHandle, preamble, fileOffset);
}
return;
}

int bytesNeeded = preambleSize + encoding.GetMaxByteCount(Math.Min(contents.Length, ChunkSize));
byte[]? rentedBytes = null;
Span<byte> bytes = bytesNeeded <= 1024 ? stackalloc byte[1024] : (rentedBytes = ArrayPool<byte>.Shared.Rent(bytesNeeded));

try
{
if (fileOffset == 0)
{
preamble.CopyTo(bytes);
}
else
{
preambleSize = 0; // don't append preamble to a non-empty file
}

Encoder encoder = encoding.GetEncoder();
ReadOnlySpan<char> remaining = contents;
while (!remaining.IsEmpty)
{
ReadOnlySpan<char> toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize));
remaining = remaining.Slice(toEncode.Length);
int encoded = encoder.GetBytes(toEncode, bytes.Slice(preambleSize), flush: remaining.IsEmpty);
Span<byte> toStore = bytes.Slice(0, preambleSize + encoded);

RandomAccess.WriteAtOffset(fileHandle, toStore, fileOffset);

fileOffset += toStore.Length;
preambleSize = 0;
}
}
finally
{
if (rentedBytes is not null)
{
ArrayPool<byte>.Shared.Return(rentedBytes);
}
}
}

private static async Task WriteToFileAsync(string path, FileMode mode, string? contents, Encoding encoding, CancellationToken cancellationToken)
{
ReadOnlyMemory<byte> preamble = encoding.GetPreamble();
int preambleSize = preamble.Length;

using SafeFileHandle fileHandle = OpenHandle(path, mode, FileAccess.Write, FileShare.Read, FileOptions.Asynchronous, GetPreallocationSize(mode, contents, encoding, preambleSize));
long fileOffset = mode == FileMode.Append && fileHandle.CanSeek ? RandomAccess.GetLength(fileHandle) : 0;

if (string.IsNullOrEmpty(contents))
{
if (preambleSize > 0 // even if the content is empty, we want to store the preamble
&& fileOffset == 0) // if we're appending to a file that already has data, don't write the preamble.
{
await RandomAccess.WriteAtOffsetAsync(fileHandle, preamble, fileOffset, cancellationToken).ConfigureAwait(false);
}
return;
}

byte[] bytes = ArrayPool<byte>.Shared.Rent(preambleSize + encoding.GetMaxByteCount(Math.Min(contents.Length, ChunkSize)));

try
{
if (fileOffset == 0)
{
preamble.CopyTo(bytes);
}
else
{
preambleSize = 0; // don't append preamble to a non-empty file
}

Encoder encoder = encoding.GetEncoder();
ReadOnlyMemory<char> remaining = contents.AsMemory();
while (!remaining.IsEmpty)
{
ReadOnlyMemory<char> toEncode = remaining.Slice(0, Math.Min(remaining.Length, ChunkSize));
remaining = remaining.Slice(toEncode.Length);
int encoded = encoder.GetBytes(toEncode.Span, bytes.AsSpan(preambleSize), flush: remaining.IsEmpty);
ReadOnlyMemory<byte> toStore = new ReadOnlyMemory<byte>(bytes, 0, preambleSize + encoded);

await RandomAccess.WriteAtOffsetAsync(fileHandle, toStore, fileOffset, cancellationToken).ConfigureAwait(false);

fileOffset += toStore.Length;
preambleSize = 0;
}
}
finally
{
ArrayPool<byte>.Shared.Return(bytes);
}
}

private static long GetPreallocationSize(FileMode mode, string? contents, Encoding encoding, int preambleSize)
{
// for a single write operation, setting preallocationSize has no perf benefit, as it requires an additional sys-call
if (contents is null || contents.Length < ChunkSize)
{
return 0;
}

// preallocationSize is ignored for Append mode, there is no need to spend cycles on GetByteCount
if (mode == FileMode.Append)
{
return 0;
}

return preambleSize + encoding.GetByteCount(contents);
}
}
}
Loading

0 comments on commit 87a44c3

Please sign in to comment.