diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs new file mode 100644 index 00000000000000..8554f1fe66a53a --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FAllocate", SetLastError = false /* this is explicitly called out in the man page */)] + internal static extern int FAllocate(SafeFileHandle fd, long offset, long length); + } +} diff --git a/src/libraries/Native/Unix/Common/pal_config.h.in b/src/libraries/Native/Unix/Common/pal_config.h.in index 67158f915a7a48..b7bd484e629ffa 100644 --- a/src/libraries/Native/Unix/Common/pal_config.h.in +++ b/src/libraries/Native/Unix/Common/pal_config.h.in @@ -34,6 +34,8 @@ #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 PRIORITY_REQUIRES_INT_WHO #cmakedefine01 KEVENT_REQUIRES_INT_PARAMS #cmakedefine01 HAVE_IOCTL diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index 5abcf2a91074dd..107c089c2cbbd0 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -89,6 +89,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_FTruncate) DllImportEntry(SystemNative_Poll) DllImportEntry(SystemNative_PosixFAdvise) + 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 96b69525471987..89fa7c2108fac5 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -662,7 +662,7 @@ int32_t SystemNative_FSync(intptr_t fd) int fileDescriptor = ToFileDescriptor(fd); int32_t result; - while ((result = + while ((result = #if defined(TARGET_OSX) && HAVE_F_FULLFSYNC fcntl(fileDescriptor, F_FULLFSYNC) #else @@ -991,6 +991,58 @@ int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, i #endif } +int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) +{ + 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); +#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_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) ; +#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; + } +#endif + + return result; +} + int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize) { return Common_Read(fd, buffer, bufferSize); @@ -1184,7 +1236,7 @@ int32_t SystemNative_CopyFile(intptr_t sourceFd, intptr_t destinationFd) #endif } // If we copied to a filesystem (eg EXFAT) that does not preserve POSIX ownership, all files appear - // to be owned by root. If we aren't running as root, then we won't be an owner of our new file, and + // to be owned by root. If we aren't running as root, then we won't be an owner of our new file, and // attempting to copy metadata to it will fail with EPERM. We have copied successfully, we just can't // copy metadata. The best thing we can do is skip copying the metadata. if (ret != 0 && errno != EPERM) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h index e82cffe8a8c3a9..313aa5a6179f6d 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.h +++ b/src/libraries/Native/Unix/System.Native/pal_io.h @@ -604,6 +604,13 @@ 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. + * + * Returns 0 on success; otherwise, the error code is returned and errno is NOT set. + */ +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 674375cdd9ec52..0cdc12cca9f0e3 100644 --- a/src/libraries/Native/Unix/configure.cmake +++ b/src/libraries/Native/Unix/configure.cmake @@ -209,6 +209,16 @@ check_symbol_exists( fcntl.h HAVE_POSIX_ADVISE) +check_symbol_exists( + posix_fallocate + fcntl.h + HAVE_POSIX_FALLOCATE) + +check_symbol_exists( + posix_fallocate64 + fcntl.h + HAVE_POSIX_FALLOCATE64) + check_symbol_exists( ioctl sys/ioctl.h diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs index 5ab668f760724c..a8ac77595566d8 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs @@ -62,7 +62,7 @@ public async Task FileOffsetIsPreservedWhenFileStreamIsCreatedFromSafeFileHandle using FileStream createdFromHandle = new FileStream(stream.SafeFileHandle, FileAccess.Write); - Assert.Equal(buffer.Length, stream.Position); + Assert.Equal(buffer.Length, stream.Position); Assert.Equal(stream.Position, createdFromHandle.Position); } @@ -187,17 +187,22 @@ public async Task WriteByteFlushesTheBufferWhenItBecomesFull() byte[] allBytes = File.ReadAllBytes(filePath); Assert.Equal(writtenBytes.ToArray(), allBytes); } - + [Fact] public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllTheDetails() { const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB string filePath = GetTestFilePath(); - IOException ex = Assert.Throws(() => new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, BufferSize, Options, tooMuch)); + + Assert.False(File.Exists(filePath)); + + IOException ex = Assert.Throws(() => new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize, Options, tooMuch)); Assert.Contains("disk was full", ex.Message); Assert.Contains(filePath, ex.Message); Assert.Contains(AllocationSize.ToString(), ex.Message); + + Assert.False(File.Exists(filePath)); } } 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 75c8c0f88d260a..ebd67d49a9c3d9 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 @@ -1,4 +1,4 @@ - + true c5ed3c1d-b572-46f1-8f96-522a85ce1179 @@ -1821,6 +1821,9 @@ Common\Interop\Unix\System.Native\Interop.PosixFAdvise.cs + + Common\Interop\Unix\System.Native\Interop.FAllocate.cs + Common\Interop\Unix\System.Native\Interop.Read.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs index 418dadc99378fd..5138b36c10b0ea 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs @@ -7,12 +7,15 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using Internal.IO; namespace System.IO.Strategies { // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static partial class FileStreamHelpers { + private const int ENOSPC_Linux = 28; + // in the future we are most probably going to introduce more strategies (io_uring etc) private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) => new Net5CompatFileStreamStrategy(handle, access, bufferSize, isAsync); @@ -35,7 +38,22 @@ internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; // Open the file and store the safe handle. - return SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); + SafeFileHandle handle = SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); + // If allocationSize has been provided for a creatable and writeable file + if (allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) + { + int allocationResult = Interop.Sys.FAllocate(handle, 0, allocationSize); + if (allocationResult == (int)Interop.Error.ENOSPC || allocationResult == ENOSPC_Linux) + { + handle.Dispose(); + Interop.Sys.Unlink(path); // remove the file to mimic Windows behaviour (atomic operation) + + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, path, allocationSize)); + } + // ignore not supported and other failures (pipe etc) + } + + return handle; } internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) => handle.IsAsync ?? defaultIsAsync;