Skip to content

Commit 2d100f1

Browse files
authored
Fix file access to align with .NET (#173)
1 parent 7471207 commit 2d100f1

4 files changed

Lines changed: 226 additions & 14 deletions

File tree

‎System.IO.FileSystem.UnitTests/FileUnitTests.cs‎

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,194 @@ public void SetAttributes_throws_if_file_does_not_exist()
739739
});
740740
}
741741

742+
[TestMethod]
743+
public void FileStream_can_open_read_only_file_for_read()
744+
{
745+
ExecuteTestAndTearDown(() =>
746+
{
747+
CreateFile(
748+
Source,
749+
BinaryContent);
750+
751+
File.SetAttributes(
752+
Source,
753+
FileAttributes.ReadOnly);
754+
755+
try
756+
{
757+
using var stream = new FileStream(
758+
Source,
759+
FileMode.Open,
760+
FileAccess.Read);
761+
762+
Assert.IsTrue(stream.CanRead, "Stream should be readable");
763+
Assert.IsFalse(stream.CanWrite, "Stream should not be writable");
764+
765+
AssertContentEquals(
766+
stream,
767+
BinaryContent);
768+
}
769+
finally
770+
{
771+
// clear the attribute so the file can be deleted on tear down
772+
File.SetAttributes(
773+
Source,
774+
FileAttributes.Normal);
775+
}
776+
});
777+
}
778+
779+
[TestMethod]
780+
public void FileStream_throws_if_read_only_file_is_opened_for_write()
781+
{
782+
ExecuteTestAndTearDown(() =>
783+
{
784+
CreateFile(
785+
Source,
786+
BinaryContent);
787+
788+
File.SetAttributes(
789+
Source,
790+
FileAttributes.ReadOnly);
791+
792+
try
793+
{
794+
AssertThrowsUnauthorizedAccess(
795+
() => new FileStream(
796+
Source,
797+
FileMode.Open,
798+
FileAccess.ReadWrite));
799+
800+
AssertThrowsUnauthorizedAccess(
801+
() => new FileStream(
802+
Source,
803+
FileMode.Open,
804+
FileAccess.Write));
805+
806+
AssertThrowsUnauthorizedAccess(
807+
() => new FileStream(
808+
Source,
809+
FileMode.Append));
810+
811+
AssertThrowsUnauthorizedAccess(
812+
() => new FileStream(
813+
Source,
814+
FileMode.Truncate));
815+
816+
// the file must be untouched
817+
AssertContentEquals(
818+
Source,
819+
BinaryContent);
820+
}
821+
finally
822+
{
823+
// clear the attribute so the file can be deleted on tear down
824+
File.SetAttributes(
825+
Source,
826+
FileAttributes.Normal);
827+
}
828+
});
829+
}
830+
831+
[TestMethod]
832+
public void FileStream_CreateNew_throws_PathAlreadyExists_for_read_only_file()
833+
{
834+
ExecuteTestAndTearDown(() =>
835+
{
836+
CreateFile(
837+
Source,
838+
BinaryContent);
839+
840+
File.SetAttributes(
841+
Source,
842+
FileAttributes.ReadOnly);
843+
844+
try
845+
{
846+
using var stream = new FileStream(
847+
Source,
848+
FileMode.CreateNew);
849+
850+
Assert.IsTrue(false, "CreateNew on an existing file should throw IOException");
851+
}
852+
catch (IOException ex)
853+
{
854+
Assert.AreEqual(
855+
(int)IOException.IOExceptionErrorCode.PathAlreadyExists,
856+
(int)ex.ErrorCode,
857+
"Unexpected IOException error code");
858+
}
859+
finally
860+
{
861+
// clear the attribute so the file can be deleted on tear down
862+
File.SetAttributes(
863+
Source,
864+
FileAttributes.Normal);
865+
}
866+
});
867+
}
868+
869+
[TestMethod]
870+
public void FileStream_can_open_for_write_after_read_only_is_cleared()
871+
{
872+
ExecuteTestAndTearDown(() =>
873+
{
874+
CreateFile(
875+
Source,
876+
BinaryContent);
877+
878+
File.SetAttributes(
879+
Source,
880+
FileAttributes.ReadOnly);
881+
882+
File.SetAttributes(
883+
Source,
884+
FileAttributes.Normal);
885+
886+
using (var stream = new FileStream(
887+
Source,
888+
FileMode.Open,
889+
FileAccess.ReadWrite))
890+
{
891+
Assert.IsTrue(stream.CanWrite, "Stream should be writable");
892+
893+
stream.Seek(0, SeekOrigin.End);
894+
895+
stream.Write(
896+
BinaryContent,
897+
0,
898+
BinaryContent.Length);
899+
}
900+
901+
var expected = new byte[BinaryContent.Length * 2];
902+
BinaryContent.CopyTo(expected, 0);
903+
BinaryContent.CopyTo(expected, BinaryContent.Length);
904+
905+
AssertContentEquals(
906+
Source,
907+
expected);
908+
});
909+
}
910+
911+
private delegate FileStream OpenFileStream();
912+
913+
private static void AssertThrowsUnauthorizedAccess(OpenFileStream open)
914+
{
915+
try
916+
{
917+
using var stream = open();
918+
919+
Assert.IsTrue(false, "Opening a read-only file with write access should throw IOException");
920+
}
921+
catch (IOException ex)
922+
{
923+
Assert.AreEqual(
924+
(int)IOException.IOExceptionErrorCode.UnauthorizedAccess,
925+
(int)ex.ErrorCode,
926+
"Unexpected IOException error code");
927+
}
928+
}
929+
742930
[TestMethod]
743931
public void WriteAllBytes_should_create_file()
744932
{

‎System.IO.FileSystem/FileStream.cs‎

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -249,29 +249,40 @@ public FileStream(
249249
(int)IOException.IOExceptionErrorCode.UnauthorizedAccess);
250250
}
251251

252+
// a read-only file can't be opened with write access
253+
// (CreateNew is left to report that the file already exists, as in full .NET)
254+
if (isReadOnly
255+
&& wantsWrite
256+
&& mode != FileMode.CreateNew)
257+
{
258+
throw new IOException(
259+
string.Empty,
260+
(int)IOException.IOExceptionErrorCode.UnauthorizedAccess);
261+
}
262+
252263
// The seek limit is 0 (the beginning of the file) for all modes except Append
253264
_seekLimit = 0;
254265

255266
switch (mode)
256267
{
257268
case FileMode.CreateNew:
258-
CreateNewFile(exists, bufferSize);
269+
CreateNewFile(exists, access, bufferSize);
259270
break;
260271

261272
case FileMode.Create:
262-
CreateFile(exists, bufferSize);
273+
CreateFile(exists, access, bufferSize);
263274
break;
264275

265276
case FileMode.Open:
266-
OpenFile(exists, bufferSize);
277+
OpenFile(exists, access, bufferSize);
267278
break;
268279

269280
case FileMode.OpenOrCreate:
270-
OpenOrCreateFile(bufferSize);
281+
OpenOrCreateFile(access, bufferSize);
271282
break;
272283

273284
case FileMode.Truncate:
274-
TruncateFile(exists, bufferSize);
285+
TruncateFile(exists, access, bufferSize);
275286
break;
276287

277288
case FileMode.Append:
@@ -636,6 +647,7 @@ private void RegisterShareInformation(
636647

637648
private void CreateNewFile(
638649
bool exists,
650+
FileAccess access,
639651
int bufferSize)
640652
{
641653
// if the file exists, IOException is thrown
@@ -648,17 +660,20 @@ private void CreateNewFile(
648660

649661
_nativeFileStream = new NativeFileStream(
650662
_fileName,
651-
bufferSize);
663+
bufferSize,
664+
(int)access);
652665
}
653666

654667
private void CreateFile(
655668
bool exists,
669+
FileAccess access,
656670
int bufferSize)
657671
{
658672
// if the file exists, it should be overwritten
659673
_nativeFileStream = new NativeFileStream(
660674
_fileName,
661-
bufferSize);
675+
bufferSize,
676+
(int)access);
662677

663678
if (exists)
664679
{
@@ -668,6 +683,7 @@ private void CreateFile(
668683

669684
private void OpenFile(
670685
bool exists,
686+
FileAccess access,
671687
int bufferSize)
672688
{
673689
// if the file does not exist, IOException/FileNotFound is thrown
@@ -680,19 +696,24 @@ private void OpenFile(
680696

681697
_nativeFileStream = new NativeFileStream(
682698
_fileName,
683-
bufferSize);
699+
bufferSize,
700+
(int)access);
684701
}
685702

686-
private void OpenOrCreateFile(int bufferSize)
703+
private void OpenOrCreateFile(
704+
FileAccess access,
705+
int bufferSize)
687706
{
688707
// if the file does not exist, it is created
689708
_nativeFileStream = new NativeFileStream(
690709
_fileName,
691-
bufferSize);
710+
bufferSize,
711+
(int)access);
692712
}
693713

694714
private void TruncateFile(
695715
bool exists,
716+
FileAccess access,
696717
int bufferSize)
697718
{
698719
// the file would be overwritten. if the file does not exist, IOException/FileNotFound is thrown
@@ -705,7 +726,8 @@ private void TruncateFile(
705726

706727
_nativeFileStream = new NativeFileStream(
707728
_fileName,
708-
bufferSize);
729+
bufferSize,
730+
(int)access);
709731

710732
_nativeFileStream.SetLength(0);
711733
}
@@ -723,7 +745,8 @@ private void AppendToFile(
723745

724746
_nativeFileStream = new NativeFileStream(
725747
_fileName,
726-
bufferSize);
748+
bufferSize,
749+
(int)access);
727750

728751
_seekLimit = _nativeFileStream.Seek(
729752
0,

‎System.IO.FileSystem/NativeFileStream.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ internal class NativeFileStream
2424
[MethodImpl(MethodImplOptions.InternalCall)]
2525
public extern NativeFileStream(
2626
string path,
27-
int bufferSize);
27+
int bufferSize,
28+
int access);
2829

2930
[MethodImpl(MethodImplOptions.InternalCall)]
3031
public extern int Read(

‎System.IO.FileSystem/Properties/AssemblyInfo.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
////////////////////////////////////////////////////////////////
1919
// update this whenever the native assembly signature changes //
20-
[assembly: AssemblyNativeVersion("100.2.0.2")]
20+
[assembly: AssemblyNativeVersion("200.1.0.4")]
2121
////////////////////////////////////////////////////////////////
2222

2323
[assembly: InternalsVisibleTo("NFUnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001001120aa3e809b3da4f65e1b1f65c0a3a1bf6335c39860ca41acb3c48de278c6b63c5df38239ec1f2e32d58cb897c8c174a5f8e78a9c0b6087d3aef373d7d0f3d9be67700fc2a5a38de1fb71b5b6f6046d841ff35abee2e0b0840a6291a312be184eb311baff5fef0ff6895b9a5f2253aed32fb06b819134f6bb9d531488a87ea2")]

0 commit comments

Comments
 (0)