Skip to content

Commit

Permalink
fix: throw UnauthorizedAccessException when replacing a read-only fil…
Browse files Browse the repository at this point in the history
…e on Windows (#408)

When replacing a read-only file on Windows, it should throw an
UnauthorizedAccessException.
  • Loading branch information
vbreuss committed Oct 6, 2023
1 parent 76932e5 commit 448b1f3
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ public IFileInfo Replace(string destinationFileName,
}
throw ExceptionFactory.DirectoryNotFound(FullName);
}))
}),
!Execute.IsWindows)
?? throw ExceptionFactory.FileNotFound(FullName);
return _fileSystem.FileInfo.New(location.FullPath);
}
Expand Down
23 changes: 18 additions & 5 deletions Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,15 @@ public IStorageContainer GetOrCreateContainer(
throw ExceptionFactory.AccessToPathDenied(source.FullPath);
}

using (_ = sourceContainer.RequestAccess(FileAccess.ReadWrite, FileShare.None,
using (_ = sourceContainer.RequestAccess(
FileAccess.ReadWrite,
FileShare.None,
ignoreMetadataErrors: ignoreMetadataErrors))
{
using (_ = destinationContainer.RequestAccess(FileAccess.ReadWrite,
FileShare.None, ignoreMetadataErrors: ignoreMetadataErrors))
using (_ = destinationContainer.RequestAccess(
FileAccess.ReadWrite,
FileShare.None,
ignoreMetadataErrors: ignoreMetadataErrors))
{
if (_containers.TryRemove(destination,
out IStorageContainer? existingDestinationContainer))
Expand All @@ -412,8 +416,17 @@ public IStorageContainer GetOrCreateContainer(
Execute.OnWindowsIf(sourceContainer.Type == FileSystemTypes.File,
() =>
{
existingSourceContainer.Attributes |=
FileAttributes targetAttributes =
existingDestinationContainer.Attributes |
FileAttributes.Archive;
if (existingSourceContainer.Attributes.HasFlag(FileAttributes
.ReadOnly))
{
targetAttributes |= FileAttributes.ReadOnly;
}
existingSourceContainer.Attributes = targetAttributes;
existingSourceContainer.CreationTime.Set(
existingDestinationContainer.CreationTime.Get(
DateTimeKind.Utc),
Expand Down Expand Up @@ -502,7 +515,7 @@ public bool TryAddContainer(
return false;
}

#endregion
#endregion

/// <inheritdoc cref="object.ToString()" />
public override string ToString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ public void
}

[SkippableTheory]
[InlineAutoData(FileAttributes.Hidden, FileAttributes.Hidden)]
[InlineAutoData(FileAttributes.System, FileAttributes.System)]
[InlineAutoData(FileAttributes.Hidden, FileAttributes.System)]
[InlineAutoData(FileAttributes.System, FileAttributes.Hidden)]
public void Replace_ShouldAddArchiveAttribute_OnWindows(
FileAttributes sourceFileAttributes,
FileAttributes destinationFileAttributes,
Expand Down Expand Up @@ -187,14 +187,48 @@ public void Replace_ShouldAddArchiveAttribute_OnWindows(

IFileInfo sut = FileSystem.FileInfo.New(sourceName);

sut.Replace(destinationName, backupName, true);
sut.Replace(destinationName, backupName);

FileSystem.File.GetAttributes(destinationName)
.Should().Be(expectedSourceAttributes);
if (Test.RunsOnMac)
{
FileSystem.File.GetAttributes(destinationName)
.Should().Be(expectedSourceAttributes);
}
else
{
FileSystem.File.GetAttributes(destinationName)
.Should().Be(expectedDestinationAttributes);
}
FileSystem.File.GetAttributes(backupName)
.Should().Be(expectedDestinationAttributes);
}

[SkippableTheory]
[AutoData]
public void Replace_WhenFileIsReadOnly_ShouldThrowUnauthorizedAccessException_OnWindows(
string sourceName,
string destinationName,
string backupName,
string sourceContents,
string destinationContents)
{
Skip.IfNot(Test.RunsOnWindows);

FileSystem.File.WriteAllText(sourceName, sourceContents);

FileSystem.File.WriteAllText(destinationName, destinationContents);
FileSystem.File.SetAttributes(destinationName, FileAttributes.ReadOnly);

IFileInfo sut = FileSystem.FileInfo.New(sourceName);

Exception? exception = Record.Exception(() =>
{
sut.Replace(destinationName, backupName);
});

exception.Should().BeException<UnauthorizedAccessException>(hResult: -2147024891);
}

[SkippableTheory]
[AutoData]
public void Replace_ShouldKeepMetadata(
Expand Down

0 comments on commit 448b1f3

Please sign in to comment.