diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixFAllocate.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs similarity index 63% rename from src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixFAllocate.cs rename to src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs index 2866ed38935bdf..71155e0f35e67e 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixFAllocate.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs @@ -9,9 +9,9 @@ internal static partial class Interop internal static partial class Sys { /// - /// Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned. + /// Returns -1 on error, 0 on success. /// - [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PosixFAllocate", SetLastError = false)] - internal static extern int PosixFAllocate(SafeFileHandle fd, long offset, long length); + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FAllocate", SetLastError = true)] + internal static extern int FAllocate(SafeFileHandle fd, long offset, long length); } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ALLOCATION_INFO.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ALLOCATION_INFO.cs new file mode 100644 index 00000000000000..0233626ee73c62 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ALLOCATION_INFO.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // Value taken from https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle#remarks: + internal const int FileAllocationInfo = 5; + + internal struct FILE_ALLOCATION_INFO + { + internal long AllocationSize; + } + } +} diff --git a/src/libraries/Native/Unix/Common/pal_config.h.in b/src/libraries/Native/Unix/Common/pal_config.h.in index d50c8d6f952976..9cffccbb0cc718 100644 --- a/src/libraries/Native/Unix/Common/pal_config.h.in +++ b/src/libraries/Native/Unix/Common/pal_config.h.in @@ -36,8 +36,7 @@ #cmakedefine01 HAVE_STRLCAT #cmakedefine01 HAVE_SHM_OPEN_THAT_WORKS_WELL_ENOUGH_WITH_MMAP #cmakedefine01 HAVE_POSIX_ADVISE -#cmakedefine01 HAVE_POSIX_FALLOCATE -#cmakedefine01 HAVE_POSIX_FALLOCATE64 +#cmakedefine01 HAVE_FALLOCATE #cmakedefine01 HAVE_PREADV #cmakedefine01 HAVE_PWRITEV #cmakedefine01 PRIORITY_REQUIRES_INT_WHO diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index c196a734c509c1..e21cfb69884b18 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -92,7 +92,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_FTruncate) DllImportEntry(SystemNative_Poll) DllImportEntry(SystemNative_PosixFAdvise) - DllImportEntry(SystemNative_PosixFAllocate) + DllImportEntry(SystemNative_FAllocate) DllImportEntry(SystemNative_Read) DllImportEntry(SystemNative_ReadLink) DllImportEntry(SystemNative_Rename) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index d86e8feb93584e..9d313dd6e99519 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -1008,83 +1008,33 @@ int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, i #endif } -int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length) +int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) { assert_msg(offset == 0, "Invalid offset value", (int)offset); int fileDescriptor = ToFileDescriptor(fd); int32_t result; -#if HAVE_POSIX_FALLOCATE64 // 64-bit Linux - while ((result = posix_fallocate64(fileDescriptor, (off64_t)offset, (off64_t)length)) == EINTR); -#elif HAVE_POSIX_FALLOCATE // 32-bit Linux - while ((result = posix_fallocate(fileDescriptor, (off_t)offset, (off_t)length)) == EINTR); +#if HAVE_FALLOCATE // Linux + while ((result = fallocate(fileDescriptor, FALLOC_FL_KEEP_SIZE, (off_t)offset, (off_t)length)) == EINTR); #elif defined(F_PREALLOCATE) // macOS fstore_t fstore; - fstore.fst_flags = F_ALLOCATECONTIG; // ensure contiguous space - fstore.fst_posmode = F_PEOFPOSMODE; // allocate from the physical end of file, as offset MUST NOT be 0 for F_VOLPOSMODE + fstore.fst_flags = F_ALLOCATEALL; // Allocate all requested space or no space at all. + fstore.fst_posmode = F_PEOFPOSMODE; // Allocate from the physical end of file. fstore.fst_offset = (off_t)offset; fstore.fst_length = (off_t)length; fstore.fst_bytesalloc = 0; // output size, can be > length while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR); - - if (result == -1) - { - // we have failed to allocate contiguous space, let's try non-contiguous - fstore.fst_flags = F_ALLOCATEALL; // all or nothing - while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR); - } -#elif defined(F_ALLOCSP) || defined(F_ALLOCSP64) // FreeBSD - #if HAVE_FLOCK64 - struct flock64 lockArgs; - int command = F_ALLOCSP64; - #else - struct flock lockArgs; - int command = F_ALLOCSP; - #endif - - lockArgs.l_whence = SEEK_SET; - lockArgs.l_start = (off_t)offset; - lockArgs.l_len = (off_t)length; - - while ((result = fcntl(fileDescriptor, command, &lockArgs)) == -1 && errno == EINTR); +#else + (void)offset; // unused + (void)length; // unused + result = -1; + errno = EOPNOTSUPP; #endif -#if defined(F_PREALLOCATE) || defined(F_ALLOCSP) || defined(F_ALLOCSP64) - // most of the Unixes implement posix_fallocate which does NOT set the last error - // fctnl does, but to mimic the posix_fallocate behaviour we just return error - if (result == -1) - { - result = errno; - } - else - { - // align the behaviour with what posix_fallocate does (change reported file size) - ftruncate(fileDescriptor, length); - } -#endif + assert(result == 0 || errno != EINVAL); - // error codes can be OS-specific, so this is why this handling is done here rather than in the managed layer - switch (result) - { - case ENOSPC: // there was not enough space - return -1; - case EFBIG: // the file was too large - return -2; - case ENODEV: // not a regular file - case ESPIPE: // a pipe - // We ignore it, as FileStream contract makes it clear that allocationSize is ignored for non-regular files. - return 0; - case EINVAL: - // We control the offset and length so they are correct. - assert_msg(length >= 0, "Invalid length value", (int)length); - // But if the underlying filesystem does not support the operation, we just ignore it and treat as a hint. - return 0; - default: - assert(result != EINTR); // it can't happen here as we retry the call on EINTR - assert(result != EBADF); // it can't happen here as this method is being called after a succesfull call to open (with write permissions) before returning the SafeFileHandle to the user - return 0; - } + return result; } int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h index 3f0db054d4368b..0e5d4cae2feb8f 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.h +++ b/src/libraries/Native/Unix/System.Native/pal_io.h @@ -620,11 +620,11 @@ PALEXPORT int32_t SystemNative_Poll(PollEvent* pollEvents, uint32_t eventCount, PALEXPORT int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, int32_t advice); /** - * Ensures that disk space is allocated. + * Preallocates disk space. * - * Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned. + * Returns 0 for success, -1 for failure. Sets errno on failure. */ -PALEXPORT int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length); +PALEXPORT int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length); /** * Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor. diff --git a/src/libraries/Native/Unix/configure.cmake b/src/libraries/Native/Unix/configure.cmake index 3500e54fc71834..1038d1b4c1f2f6 100644 --- a/src/libraries/Native/Unix/configure.cmake +++ b/src/libraries/Native/Unix/configure.cmake @@ -232,14 +232,9 @@ check_symbol_exists( HAVE_POSIX_ADVISE) check_symbol_exists( - posix_fallocate + fallocate fcntl.h - HAVE_POSIX_FALLOCATE) - -check_symbol_exists( - posix_fallocate64 - fcntl.h - HAVE_POSIX_FALLOCATE64) + HAVE_FALLOCATE) check_symbol_exists( preadv diff --git a/src/libraries/System.IO.FileSystem/src/Resources/Strings.resx b/src/libraries/System.IO.FileSystem/src/Resources/Strings.resx index 3c391ca6bcde4a..583d1c9cc366aa 100644 --- a/src/libraries/System.IO.FileSystem/src/Resources/Strings.resx +++ b/src/libraries/System.IO.FileSystem/src/Resources/Strings.resx @@ -173,6 +173,12 @@ Append access can be requested only in write-only mode. + + Preallocation size can be requested only in write mode. + + + Preallocation size can be requested only for new files. + Combining FileMode: {0} with FileAccess: {1} is invalid. diff --git a/src/libraries/System.IO.FileSystem/tests/File/Open.cs b/src/libraries/System.IO.FileSystem/tests/File/Open.cs index d014cf0efb94d8..bd9b38b121b886 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/Open.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/Open.cs @@ -42,15 +42,14 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA } } - public class File_Open_str_options_as : FileStream_ctor_options_as + public class File_Open_str_options : FileStream_ctor_options { protected override FileStream CreateFileStream(string path, FileMode mode) { return File.Open(path, new FileStreamOptions { Mode = mode, - Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite, - PreallocationSize = PreallocationSize + Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite }); } @@ -59,12 +58,23 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA return File.Open(path, new FileStreamOptions { Mode = mode, - Access = access, - PreallocationSize = PreallocationSize + Access = access }); } protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + { + return File.Open(path, + new FileStreamOptions { + Mode = mode, + Access = access, + Share = share, + Options = options, + BufferSize = bufferSize + }); + } + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { return File.Open(path, new FileStreamOptions { @@ -73,7 +83,7 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA Share = share, Options = options, BufferSize = bufferSize, - PreallocationSize = PreallocationSize + PreallocationSize = preallocationSize }); } } diff --git a/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs b/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs index a8b3619fb51cf7..120ed9aec222ef 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs @@ -7,28 +7,24 @@ namespace System.IO.Tests { // to avoid a lot of code duplication, we reuse FileStream tests - public class File_OpenHandle : FileStream_ctor_options_as + public class File_OpenHandle : FileStream_ctor_options { protected override string GetExpectedParamName(string paramName) => paramName; protected override FileStream CreateFileStream(string path, FileMode mode) { FileAccess access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite; - return new FileStream(File.OpenHandle(path, mode, access, preallocationSize: PreallocationSize), access); + return new FileStream(File.OpenHandle(path, mode, access), access); } protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access) - => new FileStream(File.OpenHandle(path, mode, access, preallocationSize: PreallocationSize), access); + => new FileStream(File.OpenHandle(path, mode, access), access); protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new FileStream(File.OpenHandle(path, mode, access, share, options, PreallocationSize), access, bufferSize, (options & FileOptions.Asynchronous) != 0); + => new FileStream(File.OpenHandle(path, mode, access, share, options), access, bufferSize, (options & FileOptions.Asynchronous) != 0); - [Fact] - public override void NegativePreallocationSizeThrows() - { - ArgumentOutOfRangeException ex = Assert.Throws( - () => File.OpenHandle("validPath", FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize: -1)); - } + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) + => new FileStream(File.OpenHandle(path, mode, access, share, options, preallocationSize), access, bufferSize, (options & FileOptions.Asynchronous) != 0); [ActiveIssue("https://github.com/dotnet/runtime/issues/53432")] [Theory, MemberData(nameof(StreamSpecifiers))] diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs index 2324a92eafce4f..36fd39a6000564 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs @@ -74,15 +74,14 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA } } - public class FileInfo_Open_options_as : FileStream_ctor_options_as + public class FileInfo_Open_options : FileStream_ctor_options { protected override FileStream CreateFileStream(string path, FileMode mode) { return new FileInfo(path).Open( new FileStreamOptions { Mode = mode, - Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite, - PreallocationSize = PreallocationSize + Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite }); } @@ -91,12 +90,23 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA return new FileInfo(path).Open( new FileStreamOptions { Mode = mode, - Access = access, - PreallocationSize = PreallocationSize + Access = access }); } protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + { + return new FileInfo(path).Open( + new FileStreamOptions { + Mode = mode, + Access = access, + Share = share, + Options = options, + BufferSize = bufferSize + }); + } + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { return new FileInfo(path).Open( new FileStreamOptions { @@ -105,7 +115,7 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA Share = share, Options = options, BufferSize = bufferSize, - PreallocationSize = PreallocationSize + PreallocationSize = preallocationSize }); } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Browser.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Browser.cs new file mode 100644 index 00000000000000..45326d885c455d --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Browser.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Tests +{ + public partial class FileStream_ctor_options + { + private static long GetAllocatedSize(FileStream fileStream) + { + return 0; + } + + private static bool SupportsPreallocation => false; + + private static bool IsGetAllocatedSizeImplemented => false; + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Unix.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Unix.cs new file mode 100644 index 00000000000000..c33c56a1fa5706 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Unix.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.IO.Tests +{ + public partial class FileStream_ctor_options + { + private static long GetAllocatedSize(FileStream fileStream) + { + bool isOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + // Call 'stat' to get the number of blocks, and size of blocks. + using var px = Process.Start(new ProcessStartInfo + { + FileName = "stat", + ArgumentList = { isOSX ? "-f" : "-c", + isOSX ? "%b %k" : "%b %B", + fileStream.Name }, + RedirectStandardOutput = true + }); + string stdout = px.StandardOutput.ReadToEnd(); + + string[] parts = stdout.Split(' '); + return long.Parse(parts[0]) * long.Parse(parts[1]); + } + + private static bool SupportsPreallocation => + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + // Mobile platforms don't support Process.Start. + private static bool IsGetAllocatedSizeImplemented => !PlatformDetection.IsMobile; + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Windows.cs similarity index 66% rename from src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs rename to src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Windows.cs index b716bcb3de25b5..2dca6baf22bd07 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Windows.cs @@ -9,8 +9,21 @@ namespace System.IO.Tests { - public partial class FileStream_ctor_options_as + public partial class FileStream_ctor_options { + private unsafe long GetAllocatedSize(FileStream fileStream) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fileStream.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); + + return info.AllocationSize; + } + + private static bool SupportsPreallocation => true; + + private static bool IsGetAllocatedSizeImplemented => true; + [Theory] [InlineData(@"\\?\")] [InlineData(@"\??\")] @@ -19,43 +32,26 @@ public void ExtendedPathsAreSupported(string prefix) { const long preallocationSize = 123; - string filePath = prefix + Path.GetFullPath(GetPathToNonExistingFile()); + string filePath = prefix + Path.GetFullPath(GetTestFilePath()); - using (var fs = new FileStream(filePath, GetOptions(FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) + using (var fs = CreateFileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.None, preallocationSize)) { - Assert.Equal(preallocationSize, fs.Length); - } - } - - [Fact] - public async Task PreallocationSizeIsIgnoredForNonSeekableFiles() - { - string pipeName = GetNamedPipeServerStreamName(); - string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}"); - - FileStreamOptions options = new() { Mode = FileMode.Open, Access = FileAccess.Write, Share = FileShare.None, PreallocationSize = 123 }; - - using (var server = new NamedPipeServerStream(pipeName, PipeDirection.In)) - using (var clienStream = new FileStream(pipePath, options)) - { - await server.WaitForConnectionAsync(); - - Assert.False(clienStream.CanSeek); + Assert.Equal(0, fs.Length); + Assert.True(GetAllocatedSize(fs) >= preallocationSize); } } [ConditionalTheory(nameof(IsFat32))] [InlineData(FileMode.Create)] [InlineData(FileMode.CreateNew)] - [InlineData(FileMode.OpenOrCreate)] public void WhenFileIsTooLargeTheErrorMessageContainsAllDetails(FileMode mode) { const long tooMuch = uint.MaxValue + 1L; // more than FAT32 max size - string filePath = GetPathToNonExistingFile(); + string filePath = GetTestFilePath(); Assert.StartsWith(Path.GetTempPath(), filePath); // this is what IsFat32 method relies on - IOException ex = Assert.Throws(() => new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, tooMuch))); + IOException ex = Assert.Throws(() => CreateFileStream(filePath, mode, FileAccess.Write, FileShare.None, bufferSize: 4096, FileOptions.None, tooMuch)); Assert.Contains(filePath, ex.Message); Assert.Contains(tooMuch.ToString(), ex.Message); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.cs new file mode 100644 index 00000000000000..c4344de517449d --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Security.Cryptography; +using Xunit; + +namespace System.IO.Tests +{ + // Don't run in parallel as the WhenDiskIsFullTheErrorMessageContainsAllDetails test + // consumes entire available free space on the disk (only on Linux, this is how posix_fallocate works) + // and if we try to run other disk-writing test in the meantime we are going to get "No space left on device" exception. + [Collection("NoParallelTests")] + public partial class FileStream_ctor_options : FileStream_ctor_str_fm_fa_fs_buffer_fo + { + protected override string GetExpectedParamName(string paramName) => "value"; + + protected override FileStream CreateFileStream(string path, FileMode mode) + => new FileStream(path, + new FileStreamOptions + { + Mode = mode, + Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite + }); + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access) + => new FileStream(path, + new FileStreamOptions + { + Mode = mode, + Access = access + }); + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + => new FileStream(path, + new FileStreamOptions + { + Mode = mode, + Access = access, + Share = share, + BufferSize = bufferSize, + Options = options + }); + + protected virtual FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) + => new FileStream(path, + new FileStreamOptions + { + Mode = mode, + Access = access, + Share = share, + BufferSize = bufferSize, + Options = options, + PreallocationSize = preallocationSize + }); + + [Fact] + public virtual void NegativePreallocationSizeThrows() + { + string filePath = GetTestFilePath(); + ArgumentOutOfRangeException ex = Assert.Throws( + () => CreateFileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: -1)); + } + + [Theory] + [InlineData(FileMode.Append)] + [InlineData(FileMode.Open)] + [InlineData(FileMode.OpenOrCreate)] + [InlineData(FileMode.Truncate)] + public void PreallocationSizeThrowsForFileModesThatOpenExistingFiles(FileMode mode) + { + Assert.Throws( + () => CreateFileStream(GetTestFilePath(), mode, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 20)); + } + + [Theory] + [InlineData(FileMode.Create)] + [InlineData(FileMode.CreateNew)] + public void PreallocationSizeThrowsForReadOnlyAccess(FileMode mode) + { + Assert.Throws( + () => CreateFileStream(GetTestFilePath(), mode, FileAccess.Read, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 20)); + } + + [Theory] + [InlineData(FileMode.Create, false)] + [InlineData(FileMode.Create, true)] + [InlineData(FileMode.CreateNew, false)] + [InlineData(FileMode.Append, false)] + [InlineData(FileMode.Append, true)] + [InlineData(FileMode.Open, true)] + [InlineData(FileMode.OpenOrCreate, true)] + [InlineData(FileMode.OpenOrCreate, false)] + [InlineData(FileMode.Truncate, true)] + public void ZeroPreallocationSizeDoesNotAllocate(FileMode mode, bool createFile) + { + string filename = GetTestFilePath(); + + if (createFile) + { + File.WriteAllText(filename, ""); + } + + using (FileStream fs = CreateFileStream(filename, mode, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 0)) + { + Assert.Equal(0, fs.Length); + if (IsGetAllocatedSizeImplemented) + { + Assert.Equal(0, GetAllocatedSize(fs)); + } + Assert.Equal(0, fs.Position); + } + } + + [Theory] + [InlineData(FileAccess.Write, FileMode.Create)] + [InlineData(FileAccess.Write, FileMode.CreateNew)] + [InlineData(FileAccess.ReadWrite, FileMode.Create)] + [InlineData(FileAccess.ReadWrite, FileMode.CreateNew)] + public void PreallocationSize(FileAccess access, FileMode mode) + { + const long preallocationSize = 123; + + using (var fs = CreateFileStream(GetTestFilePath(), mode, access, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize)) + { + Assert.Equal(0, fs.Length); + if (IsGetAllocatedSizeImplemented) + { + if (SupportsPreallocation) + { + Assert.True(GetAllocatedSize(fs) >= preallocationSize); + } + else + { + Assert.Equal(0, GetAllocatedSize(fs)); + } + } + Assert.Equal(0, fs.Position); + } + } + + [OuterLoop("Might allocate 1 TB file if there is enough space on the disk")] + // macOS fcntl doc does not mention ENOSPC error: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html + // But depending on the OS version, it might actually return it. + // Since we don't want to have unstable tests, it's better to not run it on macOS at all. + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + [Theory] + [InlineData(FileMode.Create)] + [InlineData(FileMode.CreateNew)] + public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) + { + const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB + + string filePath = GetTestFilePath(); + + IOException ex = Assert.Throws(() => CreateFileStream(filePath, mode, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, tooMuch)); + Assert.Contains(filePath, ex.Message); + Assert.Contains(tooMuch.ToString(), ex.Message); + + // ensure it was NOT created + bool exists = File.Exists(filePath); + if (exists) + { + File.Delete(filePath); + } + Assert.False(exists); + } + } + + [CollectionDefinition("NoParallelTests", DisableParallelization = true)] + public partial class NoParallelTests { } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs deleted file mode 100644 index b23ccb15739832..00000000000000 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs +++ /dev/null @@ -1,223 +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.Linq; -using System.Security.Cryptography; -using Xunit; - -namespace System.IO.Tests -{ - public abstract class FileStream_ctor_options_as_base : FileStream_ctor_str_fm_fa_fs_buffer_fo - { - protected abstract long PreallocationSize { get; } - - protected override string GetExpectedParamName(string paramName) => "value"; - - protected override FileStream CreateFileStream(string path, FileMode mode) - => new FileStream(path, - new FileStreamOptions - { - Mode = mode, - Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite, - PreallocationSize = PreallocationSize - }); - - protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access) - => new FileStream(path, - new FileStreamOptions - { - Mode = mode, - Access = access, - PreallocationSize = PreallocationSize - }); - - protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new FileStream(path, - new FileStreamOptions - { - Mode = mode, - Access = access, - Share = share, - BufferSize = bufferSize, - Options = options, - PreallocationSize = PreallocationSize - }); - - protected FileStreamOptions GetOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options, long preAllocationSize) - => new FileStreamOptions - { - Mode = mode, - Access = access, - Share = share, - Options = options, - PreallocationSize = preAllocationSize - }; - } - - public class FileStream_ctor_options_as_zero : FileStream_ctor_options_as_base - { - protected override long PreallocationSize => 0; // specifying 0 should have no effect - - protected override long InitialLength => 0; - } - - [CollectionDefinition("NoParallelTests", DisableParallelization = true)] - public partial class NoParallelTests { } - - // Don't run in parallel as the WhenDiskIsFullTheErrorMessageContainsAllDetails test - // consumes entire available free space on the disk (only on Linux, this is how posix_fallocate works) - // and if we try to run other disk-writing test in the meantime we are going to get "No space left on device" exception. - [Collection("NoParallelTests")] - public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base - { - protected override long PreallocationSize => 10; - - protected override long InitialLength => 10; - - [Fact] - public virtual void NegativePreallocationSizeThrows() - { - string filePath = GetPathToNonExistingFile(); - ArgumentOutOfRangeException ex = Assert.Throws( - () => new FileStream(filePath, GetOptions(FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, -1))); - } - - [Theory] - [InlineData(FileMode.Create, 0L)] - [InlineData(FileMode.CreateNew, 0L)] - [InlineData(FileMode.OpenOrCreate, 0L)] - public void WhenFileIsCreatedWithoutPreallocationSizeSpecifiedItsLengthIsZero(FileMode mode, long preallocationSize) - { - using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) - { - Assert.Equal(0, fs.Length); - Assert.Equal(0, fs.Position); - } - } - - [Theory] - [InlineData(FileMode.Open, 20L)] - [InlineData(FileMode.Open, 5L)] - [InlineData(FileMode.Append, 20L)] - [InlineData(FileMode.Append, 5L)] - public void PreallocationSizeIsIgnoredForFileModeOpenAndAppend(FileMode mode, long preallocationSize) - { - const int initialSize = 10; - string filePath = GetPathToNonExistingFile(); - File.WriteAllBytes(filePath, new byte[initialSize]); - - using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) - { - Assert.Equal(initialSize, fs.Length); // it has NOT been changed - Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position); - } - } - - [Theory] - [InlineData(FileMode.OpenOrCreate, 20L)] // preallocationSize > initialSize - [InlineData(FileMode.OpenOrCreate, 5L)] // preallocationSize < initialSize - public void WhenExistingFileIsBeingOpenedWithOpenOrCreateModeTheLengthRemainsUnchanged(FileMode mode, long preallocationSize) - { - const int initialSize = 10; - string filePath = GetPathToNonExistingFile(); - byte[] initialData = RandomNumberGenerator.GetBytes(initialSize); - File.WriteAllBytes(filePath, initialData); - - using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize))) - { - Assert.Equal(initialSize, fs.Length); // it was not changed - Assert.Equal(0, fs.Position); - - byte[] actualContent = new byte[initialData.Length]; - Assert.Equal(actualContent.Length, fs.Read(actualContent)); - AssertExtensions.SequenceEqual(initialData, actualContent); // the initial content was not changed - } - } - - [Theory] - [InlineData(FileMode.Create)] - [InlineData(FileMode.CreateNew)] - [InlineData(FileMode.OpenOrCreate)] - public void WhenFileIsCreatedWithPreallocationSizeSpecifiedTheLengthIsSetAndTheContentIsZeroed(FileMode mode) - { - const long preallocationSize = 123; - - using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize))) - { - Assert.Equal(preallocationSize, fs.Length); - Assert.Equal(0, fs.Position); - - AssertFileContentHasBeenZeroed(0, (int)fs.Length, fs); - } - } - - [OuterLoop("Might allocate 1 TB file if there is enough space on the disk")] - // macOS fcntl doc does not mention ENOSPC error: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html - // But depending on the OS version, it might actually return it. - // Since we don't want to have unstable tests, it's better to not run it on macOS at all. - [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] - [Theory] - [InlineData(FileMode.Create)] - [InlineData(FileMode.CreateNew)] - [InlineData(FileMode.OpenOrCreate)] - public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) - { - const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB - - string filePath = GetPathToNonExistingFile(); - - IOException ex = Assert.Throws(() => new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, tooMuch))); - Assert.Contains(filePath, ex.Message); - Assert.Contains(tooMuch.ToString(), ex.Message); - - // ensure it was NOT created - bool exists = File.Exists(filePath); - if (exists) - { - File.Delete(filePath); - } - Assert.False(exists); - } - - [Fact] - public void WhenFileIsTruncatedWithPreallocationSizeSpecifiedTheLengthIsSetAndTheContentIsZeroed() - { - const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocation size on Windows - const long preallocationSize = 100; - - string filePath = GetPathToNonExistingFile(); - File.WriteAllBytes(filePath, Enumerable.Repeat((byte)1, initialSize).ToArray()); - - using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize))) - { - Assert.Equal(preallocationSize, fs.Length); - Assert.Equal(0, fs.Position); - - AssertFileContentHasBeenZeroed(0, (int)fs.Length, fs); - } - } - - private string GetPathToNonExistingFile() - { - string filePath = GetTestFilePath(); - - if (File.Exists(filePath)) - { - File.Delete(filePath); - } - - return filePath; - } - - private static void AssertFileContentHasBeenZeroed(int from, int to, FileStream fs) - { - int expectedByteCount = to - from; - int extraByteCount = 1; - byte[] content = Enumerable.Repeat((byte)1, expectedByteCount + extraByteCount).ToArray(); - fs.Position = from; - Assert.Equal(expectedByteCount, fs.Read(content)); - Assert.All(content.SkipLast(extraByteCount), @byte => Assert.Equal(0, @byte)); - Assert.Equal(to, fs.Position); - } - } -} diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs index 47e3b492373161..cd3b18da034228 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs @@ -13,8 +13,6 @@ protected virtual FileStream CreateFileStream(string path, FileMode mode) return new FileStream(path, mode); } - protected virtual long InitialLength => 0; - protected virtual string GetExpectedParamName(string paramName) => paramName; [Fact] @@ -97,7 +95,7 @@ public void FileModeCreateExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Create)) { // Ensure that the file was re-created - Assert.Equal(InitialLength, fs.Length); + Assert.Equal(0, fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -148,7 +146,7 @@ public void FileModeOpenExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Open)) { // Ensure that the file was re-opened - Assert.Equal(Math.Max(1L, InitialLength), fs.Length); + Assert.Equal(1, fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -177,7 +175,7 @@ public void FileModeOpenOrCreateExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.OpenOrCreate)) { // Ensure that the file was re-opened - Assert.Equal(Math.Max(1L, InitialLength), fs.Length); + Assert.Equal(1, fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -204,7 +202,7 @@ public void FileModeTruncateExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Truncate)) { // Ensure that the file was re-opened and truncated - Assert.Equal(InitialLength, fs.Length); + Assert.Equal(0, fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -246,7 +244,7 @@ public virtual void FileModeAppendExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Append)) { // Ensure that the file was re-opened and position set to end - Assert.Equal(Math.Max(1L, InitialLength), fs.Length); + Assert.Equal(1, fs.Length); long position = fs.Position; Assert.Equal(fs.Length, position); diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj index d9fcde23a91b79..54b9c2622937d7 100644 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj @@ -23,6 +23,8 @@ + + diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index a9bedb8bd312bc..ae57e79319a5f3 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -24,7 +24,7 @@ - + @@ -71,12 +71,13 @@ + - + @@ -86,6 +87,8 @@ + + @@ -99,6 +102,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 777997444848fe..1edb65fe58afbb 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -323,20 +323,24 @@ private void Init(string path, FileMode mode, FileAccess access, FileShare share } } - // If preallocationSize has been provided for a creatable and writeable file - if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode, this)) + if (preallocationSize > 0 && Interop.Sys.FAllocate(this, 0, preallocationSize) < 0) { - int fallocateResult = Interop.Sys.PosixFAllocate(this, 0, preallocationSize); - if (fallocateResult != 0) + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + + // Only throw for errors that indicate there is not enough space. + if (errorInfo.Error == Interop.Error.EFBIG || + errorInfo.Error == Interop.Error.ENOSPC) { Dispose(); - Interop.Sys.Unlink(path!); // remove the file to mimic Windows behaviour (atomic operation) - Debug.Assert(fallocateResult == -1 || fallocateResult == -2); - throw new IOException(SR.Format( - fallocateResult == -1 ? SR.IO_DiskFull_Path_AllocationSize : SR.IO_FileTooLarge_Path_AllocationSize, - path, - preallocationSize)); + // Delete the file we've created. + Debug.Assert(mode == FileMode.Create || mode == FileMode.CreateNew); + Interop.Sys.Unlink(path!); + + throw new IOException(SR.Format(errorInfo.Error == Interop.Error.EFBIG + ? SR.IO_FileTooLarge_Path_AllocationSize + : SR.IO_DiskFull_Path_AllocationSize, + path, preallocationSize)); } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 3dde1e2e89a6b8..3352f0194e73b9 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -34,7 +34,7 @@ internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileA // of converting DOS to NT file paths (RtlDosPathNameToRelativeNtPathName_U_WithStatus is not documented) SafeFileHandle fileHandle = CreateFile(fullPath, mode, access, share, options); - if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode, fileHandle)) + if (preallocationSize > 0) { Preallocate(fullPath, preallocationSize, fileHandle); } @@ -108,18 +108,33 @@ private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode, private static unsafe void Preallocate(string fullPath, long preallocationSize, SafeFileHandle fileHandle) { - // preallocationSize must be ignored for non-seekable files, unsupported file systems - // and other failures other than ERROR_DISK_FULL and ERROR_FILE_TOO_LARGE - if (!FileStreamHelpers.TrySetFileLength(fileHandle, preallocationSize, out int errorCode) - && errorCode == Interop.Errors.ERROR_DISK_FULL || errorCode == Interop.Errors.ERROR_FILE_TOO_LARGE) - { - // Windows does not modify the file size if the request can't be satisfied in atomic way. - // posix_fallocate (Unix) implementation might consume all available disk space and fail after that. - // To ensure that the behaviour is the same for every OS (no incomplete or empty file), we close the handle and delete the file. - fileHandle.Dispose(); - Interop.Kernel32.DeleteFile(fullPath); - - throw new IOException(SR.Format(errorCode == Interop.Errors.ERROR_DISK_FULL ? SR.IO_DiskFull_Path_AllocationSize : SR.IO_FileTooLarge_Path_AllocationSize, fullPath, preallocationSize)); + var allocationInfo = new Interop.Kernel32.FILE_ALLOCATION_INFO + { + AllocationSize = preallocationSize + }; + + if (!Interop.Kernel32.SetFileInformationByHandle( + fileHandle, + Interop.Kernel32.FileAllocationInfo, + &allocationInfo, + (uint)sizeof(Interop.Kernel32.FILE_ALLOCATION_INFO))) + { + int errorCode = Marshal.GetLastPInvokeError(); + + // Only throw for errors that indicate there is not enough space. + if (errorCode == Interop.Errors.ERROR_DISK_FULL || + errorCode == Interop.Errors.ERROR_FILE_TOO_LARGE) + { + fileHandle.Dispose(); + + // Delete the file we've created. + Interop.Kernel32.DeleteFile(fullPath); + + throw new IOException(SR.Format(errorCode == Interop.Errors.ERROR_DISK_FULL + ? SR.IO_DiskFull_Path_AllocationSize + : SR.IO_FileTooLarge_Path_AllocationSize, + fullPath, preallocationSize)); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 1e16e6b577abf7..c651da612d53c4 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -1092,6 +1092,12 @@ Append access can be requested only in write-only mode. + + Preallocation size can be requested only in write mode. + + + Preallocation size can be requested only for new files. + Type of argument is not compatible with the generic comparer. 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 2a757e5f52589c..25f933a1e5b312 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 @@ -1457,6 +1457,9 @@ Common\Interop\Windows\Kernel32\Interop.FILE_BASIC_INFO.cs + + Common\Interop\Windows\Kernel32\Interop.FILE_ALLOCATION_INFO.cs + Common\Interop\Windows\Kernel32\Interop.FILE_END_OF_FILE_INFO.cs @@ -2036,8 +2039,8 @@ Common\Interop\Unix\System.Native\Interop.PosixFAdvise.cs - - Common\Interop\Unix\System.Native\Interop.PosixFAllocate.cs + + Common\Interop\Unix\System.Native\Interop.FAllocate.cs Common\Interop\Unix\System.Native\Interop.PRead.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index b60bb9d79e3a08..b97194b86c5f0b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -197,6 +197,11 @@ public FileStream(string path, FileStreamOptions options) } } + if (options.PreallocationSize > 0) + { + FileStreamHelpers.ValidateArgumentsForPreallocation(options.Mode, options.Access); + } + FileStreamHelpers.SerializationGuard(options.Access); _strategy = FileStreamHelpers.ChooseStrategy( diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs index 981776da32b81d..3db2174d3fbd14 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs @@ -58,12 +58,6 @@ e is UnauthorizedAccessException || e is NotSupportedException || (e is ArgumentException && !(e is ArgumentNullException)); - internal static bool ShouldPreallocate(long preallocationSize, FileAccess access, FileMode mode, SafeFileHandle fileHandle) - => preallocationSize > 0 - && (access & FileAccess.Write) != 0 - && mode != FileMode.Open && mode != FileMode.Append - && (mode != FileMode.OpenOrCreate || (fileHandle.CanSeek && RandomAccess.GetFileLength(fileHandle) == 0)); // allow to extend only new files - internal static void ValidateArguments(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { if (path == null) @@ -126,9 +120,31 @@ internal static void ValidateArguments(string path, FileMode mode, FileAccess ac throw new ArgumentException(SR.Argument_InvalidAppendMode, nameof(access)); } + if (preallocationSize > 0) + { + ValidateArgumentsForPreallocation(mode, access); + } + SerializationGuard(access); } + internal static void ValidateArgumentsForPreallocation(FileMode mode, FileAccess access) + { + // The user will be writing into the preallocated space. + if ((access & FileAccess.Write) == 0) + { + throw new ArgumentException(SR.Argument_InvalidPreallocateAccess, nameof(access)); + } + + // Only allow preallocation for newly created/overwritten files. + // When we fail to preallocate, we'll remove the file. + if (mode != FileMode.Create && + mode != FileMode.CreateNew) + { + throw new ArgumentException(SR.Argument_InvalidPreallocateMode, nameof(mode)); + } + } + internal static void SerializationGuard(FileAccess access) { if ((access & FileAccess.Write) == FileAccess.Write)