Skip to content

Commit

Permalink
FileMode.OpenOrCreate: don't change existing files
Browse files Browse the repository at this point in the history
  • Loading branch information
adamsitnik committed Sep 13, 2021
1 parent e9efb9b commit 81fbd92
Show file tree
Hide file tree
Showing 7 changed files with 15 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

Expand All @@ -13,6 +12,6 @@ internal static partial class Sys
/// Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned.
/// </summary>
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PosixFAllocate", SetLastError = false)]
internal static extern int PosixFAllocate(SafeFileHandle fd, long offset, long length, FileMode mode);
internal static extern int PosixFAllocate(SafeFileHandle fd, long offset, long length);
}
}
20 changes: 1 addition & 19 deletions src/libraries/Native/Unix/System.Native/pal_io.c
Original file line number Diff line number Diff line change
Expand Up @@ -1008,29 +1008,11 @@ 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 mode)
int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length)
{
assert_msg(offset == 0, "Invalid offset value", (int)offset);

int fileDescriptor = ToFileDescriptor(fd);
#if !HAVE_POSIX_FALLOCATE64 && !HAVE_POSIX_FALLOCATE
// posix_fallocate does not allow for shrinking file
// when it's not available, we need to ensure that the file won't be shrinked
if (mode == 4) // OpenOrCreate
{
struct stat_ fileStat;
int fstatResult;
while ((fstatResult = fstat_(fileDescriptor, &fileStat)) < 0 && errno == EINTR);

if (fstatResult != 0 || (fileStat.st_mode & S_IFMT) != S_IFREG || (int64_t)fileStat.st_size >= length)
{
return 0;
}
}
#else
(void)mode;
#endif

int32_t result;
#if HAVE_POSIX_FALLOCATE64 // 64-bit Linux
while ((result = posix_fallocate64(fileDescriptor, (off64_t)offset, (off64_t)length)) == EINTR);
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Native/Unix/System.Native/pal_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ PALEXPORT int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t
*
* Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned.
*/
PALEXPORT int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length, int32_t mode);
PALEXPORT int32_t SystemNative_PosixFAllocate(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ public void PreallocationSizeIsIgnoredForFileModeOpenAndAppend(FileMode mode, lo
}

[Theory]
[InlineData(FileMode.OpenOrCreate, 20L)] // this can extend the file
[InlineData(FileMode.OpenOrCreate, 5L)] // this should NOT shink the file
public void WhenFileIsBeingOpenedWithOpenOrCreateModeTheLengthCanBeOnlyExtended(FileMode mode, long preallocationSize)
[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();
Expand All @@ -125,14 +125,12 @@ public void WhenFileIsBeingOpenedWithOpenOrCreateModeTheLengthCanBeOnlyExtended(

using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize)))
{
Assert.Equal(Math.Max(initialSize, preallocationSize), fs.Length); // it was not shrinked
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

AssertFileContentHasBeenZeroed(initialSize, (int)fs.Length, fs);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,9 @@ 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))
if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode, this))
{
int fallocateResult = Interop.Sys.PosixFAllocate(this, 0, preallocationSize, mode);
int fallocateResult = Interop.Sys.PosixFAllocate(this, 0, preallocationSize);
if (fallocateResult != 0)
{
Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode, fileHandle))
{
Preallocate(fullPath, preallocationSize, fileHandle, mode);
}
Expand Down Expand Up @@ -106,20 +106,8 @@ private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode,
return 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
private static unsafe void Preallocate(string fullPath, long preallocationSize, SafeFileHandle fileHandle, FileMode mode)
{
if (mode == FileMode.OpenOrCreate)
{
Interop.Kernel32.FILE_STANDARD_INFO info;
if (!Interop.Kernel32.GetFileInformationByHandleEx(fileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))
|| info.EndOfFile >= preallocationSize) // existing files must NOT be shrinked
{
return;
}
}

var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO
{
EndOfFile = preallocationSize
Expand All @@ -131,6 +119,8 @@ private static unsafe void Preallocate(string fullPath, long preallocationSize,
&eofInfo,
(uint)sizeof(Interop.Kernel32.FILE_END_OF_FILE_INFO)))
{
// preallocationSize must be ignored for non-seekable files, unsupported file systems
// and other failures other than ERROR_DISK_FULL and ERROR_FILE_TOO_LARGE
int errorCode = Marshal.GetLastPInvokeError();
if (errorCode != Interop.Errors.ERROR_DISK_FULL && errorCode != Interop.Errors.ERROR_FILE_TOO_LARGE)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ e is UnauthorizedAccessException ||
e is NotSupportedException ||
(e is ArgumentException && !(e is ArgumentNullException));

internal static bool ShouldPreallocate(long preallocationSize, FileAccess access, FileMode mode)
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.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)
{
Expand Down

0 comments on commit 81fbd92

Please sign in to comment.