From aaf18151e00fde63b0e54fd9ffb248183b39a8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Tue, 23 Jul 2024 14:03:02 +0100 Subject: [PATCH] Add nativefilestream and several new methods to `File` and `FileStream` (#98) --- .../DirectoryUnitTests.cs | 405 +++++++++++ .../FileSystemUnitTestsBase.cs | 105 +++ .../FileUnitTests.cs | 572 ++++++++++++---- .../PathInternalUnitTests.cs | 28 +- .../PathUnitTests.cs | 161 +++-- .../System.IO.FileSystem.UnitTests.nfproj | 9 +- System.IO.FileSystem/Directory.cs | 347 +++++++--- System.IO.FileSystem/DirectoryInfo.cs | 197 ++++++ System.IO.FileSystem/DriveInfo.cs | 69 +- System.IO.FileSystem/File.cs | 581 ++++++++++------ System.IO.FileSystem/FileAttributes.cs | 5 + System.IO.FileSystem/FileInfo.cs | 111 +++ System.IO.FileSystem/FileStream.cs | 643 +++++++++++------- System.IO.FileSystem/FileSystemInfo.cs | 115 ++++ System.IO.FileSystem/FileSystemManager.cs | 236 +++++++ System.IO.FileSystem/NativeFileInfo.cs | 14 + System.IO.FileSystem/NativeFileStream.cs | 66 ++ System.IO.FileSystem/NativeFindFile.cs | 17 + System.IO.FileSystem/NativeIO.cs | 58 ++ System.IO.FileSystem/Path.cs | 5 +- System.IO.FileSystem/PathInternal.cs | 87 ++- .../Properties/AssemblyInfo.cs | 2 +- .../System.IO.FileSystem.nfproj | 15 +- ...ventArgs.cs => RemovableDriveEventArgs.cs} | 33 +- .../nanoFramework/StorageEventManager.cs | 104 +-- 25 files changed, 3143 insertions(+), 842 deletions(-) create mode 100644 System.IO.FileSystem.UnitTests/DirectoryUnitTests.cs create mode 100644 System.IO.FileSystem.UnitTests/FileSystemUnitTestsBase.cs create mode 100644 System.IO.FileSystem/DirectoryInfo.cs create mode 100644 System.IO.FileSystem/FileInfo.cs create mode 100644 System.IO.FileSystem/FileSystemInfo.cs create mode 100644 System.IO.FileSystem/FileSystemManager.cs create mode 100644 System.IO.FileSystem/NativeFileInfo.cs create mode 100644 System.IO.FileSystem/NativeFileStream.cs create mode 100644 System.IO.FileSystem/NativeFindFile.cs create mode 100644 System.IO.FileSystem/NativeIO.cs rename System.IO.FileSystem/nanoFramework/{RemovableDeviceEventArgs.cs => RemovableDriveEventArgs.cs} (62%) diff --git a/System.IO.FileSystem.UnitTests/DirectoryUnitTests.cs b/System.IO.FileSystem.UnitTests/DirectoryUnitTests.cs new file mode 100644 index 0000000..4b9382f --- /dev/null +++ b/System.IO.FileSystem.UnitTests/DirectoryUnitTests.cs @@ -0,0 +1,405 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; + +namespace System.IO.FileSystem.UnitTests +{ + [TestClass] + public class DirectoryUnitTests : FileSystemUnitTestsBase + { + [Setup] + public void Setup() + { + Assert.SkipTest("These test will only run on real hardware. Comment out this line if you are testing on real hardware."); + + RemovableDrivesHelper(); + } + + [TestMethod] + public void TestCreateDirectory() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + Directory.CreateDirectory(path); + + Assert.IsTrue(Directory.Exists(path)); + + // Clean up after the test + Directory.Delete(path); + } + + [TestMethod] + public void TestDeleteDirectory() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + Directory.CreateDirectory(path); + Directory.Delete(path); + + Assert.IsFalse(Directory.Exists(path)); + } + + [TestMethod] + public void TestDeleteDirectoryRecursive() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + Directory.CreateDirectory(path); + + File.Create($@"{path}file1.txt").Close(); + File.Create($@"{path}file2.txt").Close(); + + Directory.CreateDirectory($@"{path}subdir\"); + + File.Create($@"{path}subdir\file3.txt").Close(); + File.Create($@"{path}subdir\file4.txt").Close(); + + Directory.Delete(path, true); + + Assert.IsFalse(Directory.Exists(path)); + } + + [TestMethod] + public void TestEnumerateDirectories() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + Directory.CreateDirectory(path); + Directory.CreateDirectory($@"{path}subdir1\"); + Directory.CreateDirectory($@"{path}subdir2\"); + Directory.CreateDirectory($@"{path}subdir3\"); + + var directories = Directory.GetDirectories(path); + + Assert.AreEqual(3, directories.Length); + + // Clean up after the test + Directory.Delete(path, true); + } + + [TestMethod] + public void TestEnumerateFiles() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + Directory.CreateDirectory(path); + + File.Create($@"{path}file1.txt").Close(); + File.Create($@"{path}file2.txt").Close(); + File.Create($@"{path}file3.txt").Close(); + + var files = Directory.GetFiles(path); + + Assert.AreEqual(3, files.Length); + + // Clean up after the test + Directory.Delete(path, true); + } + + [TestMethod] + public void TestMoveDirectory() + { + string path = @$"{Root}temp\testdir\"; + string newPath = @$"{Root}temp\testdir2\"; + + // make sure both directories doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + if (Directory.Exists(newPath)) + { + Directory.Delete(newPath, true); + } + + // create the directory and some files + Directory.CreateDirectory(path); + + File.Create($@"{path}file1.txt").Close(); + File.Create($@"{path}file2.txt").Close(); + File.Create($@"{path}file3.txt").Close(); + + // perform the move + Directory.Move(path, newPath); + + // check if the directory was moved + Assert.IsFalse(Directory.Exists(path)); + // check if the directory exists in the new location + Assert.IsTrue(Directory.Exists(newPath)); + // check if the files were moved + Assert.AreEqual(3, Directory.GetFiles(newPath).Length); + + // Clean up after the test + Directory.Delete(newPath, true); + } + + [TestMethod] + public void TestMoveDirectoryWithFiles() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + Directory.CreateDirectory(path); + + File.Create($@"{path}file1.txt").Close(); + File.Create($@"{path}file2.txt").Close(); + File.Create($@"{path}file3.txt").Close(); + + string newPath = @$"{Root}temp\testdir2\"; + + Directory.Move(path, newPath); + + Assert.IsFalse(Directory.Exists(path)); + Assert.IsTrue(Directory.Exists(newPath)); + Assert.AreEqual(3, Directory.GetFiles(newPath).Length); + + // Clean up after the test + Directory.Delete(newPath, true); + } + + [TestMethod] + public void TestMoveDirectoryWithSubdirectories() + { + string path = @$"{Root}temp\testdir\"; + string newPath = @$"{Root}temp\testdir2\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + if (Directory.Exists(newPath)) + { + Directory.Delete(newPath, true); + } + + Directory.CreateDirectory(path); + + File.Create($@"{path}file1.txt").Close(); + File.Create($@"{path}file2.txt").Close(); + File.Create($@"{path}file3.txt").Close(); + + Directory.CreateDirectory($@"{path}subdir1\"); + Directory.CreateDirectory($@"{path}subdir2\"); + Directory.CreateDirectory($@"{path}subdir3\"); + + Directory.Move(path, newPath); + + Assert.IsFalse(Directory.Exists(path)); + Assert.IsTrue(Directory.Exists(newPath)); + Assert.AreEqual(3, Directory.GetFiles(newPath).Length); + Assert.AreEqual(3, Directory.GetDirectories(newPath).Length); + + // Clean up after the test + Directory.Delete(newPath, true); + } + + [TestMethod] + public void TestMoveDirectoryWithSubdirectoriesAndFiles() + { + string path = @$"{Root}temp\testdir\"; + string newPath = @$"{Root}temp\testdir2\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + if (Directory.Exists(newPath)) + { + Directory.Delete(newPath, true); + } + + Directory.CreateDirectory(path); + + File.Create($@"{path}file1.txt").Close(); + File.Create($@"{path}file2.txt").Close(); + File.Create($@"{path}file3.txt").Close(); + + Directory.CreateDirectory($@"{path}subdir1\"); + Directory.CreateDirectory($@"{path}subdir2\"); + Directory.CreateDirectory($@"{path}subdir3\"); + + File.Create($@"{path}subdir1\file1.txt").Close(); + File.Create($@"{path}subdir1\file2.txt").Close(); + File.Create($@"{path}subdir1\file3.txt").Close(); + File.Create($@"{path}subdir2\file1.txt").Close(); + File.Create($@"{path}subdir2\file2.txt").Close(); + File.Create($@"{path}subdir2\file3.txt").Close(); + File.Create($@"{path}subdir3\file1.txt").Close(); + File.Create($@"{path}subdir3\file2.txt").Close(); + File.Create($@"{path}subdir3\file3.txt").Close(); + + Directory.Move(path, newPath); + + Assert.IsFalse(Directory.Exists(path)); + Assert.IsTrue(Directory.Exists(newPath)); + Assert.AreEqual(3, Directory.GetFiles(newPath).Length); + Assert.AreEqual(3, Directory.GetDirectories(newPath).Length); + Assert.AreEqual(3, Directory.GetFiles($@"{newPath}subdir1").Length); + Assert.AreEqual(3, Directory.GetFiles($@"{newPath}subdir2").Length); + Assert.AreEqual(3, Directory.GetFiles($@"{newPath}subdir3").Length); + + // Clean up after the test + Directory.Delete(newPath, true); + } + + [TestMethod] + public void TestMoveDirectoryWithSubdirectoriesAndFilesAndOverwrite() + { + string path = @$"{Root}temp\testdir\"; + string newPath = @$"{Root}temp\testdir2\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + if (Directory.Exists(newPath)) + { + Directory.Delete(newPath, true); + } + + Directory.CreateDirectory(path); + File.Create($@"{path}file1.txt").Close(); + File.Create($@"{path}file2.txt").Close(); + File.Create($@"{path}file3.txt").Close(); + Directory.CreateDirectory($@"{path}subdir1\"); + Directory.CreateDirectory($@"{path}subdir2\"); + Directory.CreateDirectory($@"{path}subdir3\"); + File.Create($@"{path}subdir1\file1.txt").Close(); + File.Create($@"{path}subdir1\file2.txt").Close(); + File.Create($@"{path}subdir1\file3.txt").Close(); + File.Create($@"{path}subdir2\file1.txt").Close(); + File.Create($@"{path}subdir2\file2.txt").Close(); + File.Create($@"{path}subdir2\file3.txt").Close(); + File.Create($@"{path}subdir3\file1.txt").Close(); + File.Create($@"{path}subdir3\file2.txt").Close(); + File.Create($@"{path}subdir3\file3.txt").Close(); + + Directory.Move(path, newPath); + + Assert.IsFalse(Directory.Exists(path), "Origin path exists after move and it shoudln't."); + Assert.IsTrue(Directory.Exists(newPath), "Destination doesn't exist and it should."); + Assert.AreEqual(3, Directory.GetFiles(newPath).Length, $"Wrong file count @ {newPath}."); + Assert.AreEqual(3, Directory.GetDirectories(newPath).Length, $"Wrong directory count @ {newPath}."); + Assert.AreEqual(3, Directory.GetFiles($@"{newPath}subdir1").Length, $@"Wrong file count @ {newPath}\subdir1."); + Assert.AreEqual(3, Directory.GetFiles($@"{newPath}subdir2").Length, $@"Wrong file count @ {newPath}\subdir2."); + Assert.AreEqual(3, Directory.GetFiles($@"{newPath}subdir3").Length, $@"Wrong file count @ {newPath}\subdir3."); + + // Clean up after the test + Directory.Delete(newPath, true); + } + + [TestMethod] + public void TestGetDirectoryRoot() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + var directoryInfo = Directory.CreateDirectory(path); + + Assert.AreEqual(Root, directoryInfo.Root.ToString()); + + // Clean up after the test + Directory.Delete(path); + } + + [TestMethod] + public void TestGetParentDirectory() + { + string path = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + + var directoryInfo = Directory.CreateDirectory(path); + + Assert.AreEqual(Path.Combine(Root, "temp"), directoryInfo.Parent.ToString()); + + // Clean up after the test + Directory.Delete(path); + } + + [TestMethod] + public void TestGetCurrentDirectory() + { + string currentDirectory = Directory.GetCurrentDirectory(); + + Assert.AreEqual(NativeIO.FSRoot, currentDirectory); + } + + [TestMethod] + public void TestSetCurrentDirectory() + { + string newCurrentDirectory = @$"{Root}temp\testdir\"; + + // make sure the directory doesn't exist + if (Directory.Exists(newCurrentDirectory)) + { + Directory.Delete(newCurrentDirectory, true); + } + + Directory.CreateDirectory(newCurrentDirectory); + + Directory.SetCurrentDirectory(newCurrentDirectory); + + string currentDirectory = Directory.GetCurrentDirectory(); + + Assert.AreEqual(newCurrentDirectory, currentDirectory); + + // Clean up after the test + Directory.SetCurrentDirectory(Root); + Directory.Delete(newCurrentDirectory); + } + } +} diff --git a/System.IO.FileSystem.UnitTests/FileSystemUnitTestsBase.cs b/System.IO.FileSystem.UnitTests/FileSystemUnitTestsBase.cs new file mode 100644 index 0000000..5c09c7b --- /dev/null +++ b/System.IO.FileSystem.UnitTests/FileSystemUnitTestsBase.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.System.IO.FileSystem; +using nanoFramework.TestFramework; +using System.Threading; + +namespace System.IO.FileSystem.UnitTests +{ + [TestClass] + public abstract class FileSystemUnitTestsBase + { + ///////////////////////////////////////////////////////////////////// + // The test execution can be configured using the following fields // + ///////////////////////////////////////////////////////////////////// + + // set to the number of drives available in the target + internal const int _numberOfDrives = 1; + + // set to the root of the drive to use for the tests + // D: SD card + // E: USB mass storage + // I: and J: internal flash + internal const string Root = @"I:\"; + + // set to true to wait for removable drive(s) to be mounted + internal const bool _waitForRemovableDrive = false; + + // set to true to have SPI SD card mounted + internal const bool _configAndMountSdCard = false; + + ////////////////////////////////////////////////// + + private SDCard _mycardBacking; + + internal SDCard MyCard + { + set + { + _mycardBacking = value; + } + + get + { + _mycardBacking ??= InitializeSDCard(); + + return _mycardBacking; + } + } + + /// + /// Initializes the SD card. Can be overridden in the derived class to provide specific initialization. + /// + /// + protected SDCard InitializeSDCard() + { + // Example initialization logic + SDCard.SDCardMmcParameters parameters = new SDCard.SDCardMmcParameters + { + dataWidth = SDCard.SDDataWidth._4_bit, + enableCardDetectPin = true, + cardDetectPin = 21 + }; + + return new SDCard(parameters); + } + + /// + /// Helper method to be called from the tests to handle removable drives. + /// + internal void RemovableDrivesHelper() + { + if (_configAndMountSdCard) + { + TryToMountAgain: + + try + { + MyCard.Mount(); + } + catch (Exception ex) + { + OutputHelper.WriteLine($"SDCard mount failed: {ex.Message}"); + + Thread.Sleep(TimeSpan.FromSeconds(2)); + + MyCard = null; + + goto TryToMountAgain; + } + } + + if (_waitForRemovableDrive) + { + // wait until all removable drives are mounted + while (DriveInfo.GetDrives().Length < _numberOfDrives) + { + Thread.Sleep(1000); + } + } + } + } +} diff --git a/System.IO.FileSystem.UnitTests/FileUnitTests.cs b/System.IO.FileSystem.UnitTests/FileUnitTests.cs index 7992d5a..89d6904 100644 --- a/System.IO.FileSystem.UnitTests/FileUnitTests.cs +++ b/System.IO.FileSystem.UnitTests/FileUnitTests.cs @@ -1,18 +1,16 @@ -using System.Text; +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + using nanoFramework.TestFramework; +using System.Text; namespace System.IO.FileSystem.UnitTests { [TestClass] - public class FileUnitTests + public class FileUnitTests : FileSystemUnitTestsBase { - [Setup] - public void Setup() - { - Assert.SkipTest("These test will only run on real hardware. Comment out this line if you are testing on real hardware."); - } - - private const string Root = @"I:\"; private static readonly string Destination = $"{Root}{nameof(FileUnitTests)}-Destination.test"; private static readonly string Source = $"{Root}{nameof(FileUnitTests)}-Source.test"; @@ -20,39 +18,73 @@ public void Setup() private static readonly byte[] EmptyContent = new byte[0]; private const string TextContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + [Setup] + public void Setup() + { + Assert.SkipTest("These test will only run on real hardware. Comment out this line if you are testing on real hardware."); + + RemovableDrivesHelper(); + } + #region Test helpers + private static void AssertContentEquals(string path, byte[] expected) { - using var inputStream = new FileStream(path, FileMode.Open, FileAccess.Read); - AssertContentEquals(inputStream, expected); - } + var buffer = File.ReadAllBytes(path); + AssertContentEquals( + buffer, + expected); + } private static void AssertContentEquals(Stream stream, byte[] expected) { - Assert.AreEqual(expected!.Length, stream.Length, "File is not the correct length."); + var buffer = new byte[expected.Length]; - var content = new byte[stream.Length]; - stream.Read(content, 0, content.Length); + stream.Read( + buffer, + 0, + buffer.Length); + + AssertContentEquals( + buffer, + expected); + } + + private static void AssertContentEquals(byte[] content, byte[] expected) + { + Assert.AreEqual( + expected.Length, + content.Length, + "File content has wrong length."); for (var i = 0; i < content.Length; i++) { - Assert.AreEqual(expected[i], content[i], "File does not contain the expected data."); + Assert.AreEqual( + expected[i], + content[i], + "File does not contain the expected data."); } } private void AssertContentEquals(string path, string content) { - AssertContentEquals(path, Encoding.UTF8.GetBytes(content)); + AssertContentEquals( + path, + Encoding.UTF8.GetBytes(content)); } private static void AssertFileDoesNotExist(string path) { - Assert.IsFalse(File.Exists(path), $"'{path}' exists when it shouldn't."); + Assert.IsFalse( + File.Exists(path), + $"'{path}' exists when it shouldn't."); } private static void AssertFileExists(string path) { - Assert.IsTrue(File.Exists(path), $"'{path}' does not exist when it should."); + Assert.IsTrue( + File.Exists(path), + $"'{path}' does not exist when it should."); } /// @@ -60,16 +92,17 @@ private static void AssertFileExists(string path) /// private static void CreateFile(string path, byte[] content) { - File.Create(path); - - if (content!.Length > 0) + if (content is not null) { - using var outputStream = new FileStream(path, FileMode.Open, FileAccess.Write); - outputStream.Write(content, 0, content.Length); - } + OutputHelper.WriteLine($"Creating file: {path}..."); - AssertFileExists(path); - AssertContentEquals(path, content); + File.WriteAllBytes( + path, + content); + + AssertFileExists(path); + AssertContentEquals(path, content); + } } /// @@ -77,11 +110,17 @@ private static void CreateFile(string path, byte[] content) /// private static void CreateFile(string path, string content) { - CreateFile(path, Encoding.UTF8.GetBytes(content)); + OutputHelper.WriteLine($"Creating file: {path}..."); + + CreateFile( + path, + Encoding.UTF8.GetBytes(content)); } private static void DeleteFile(string path) { + OutputHelper.WriteLine($"Deleting file: {path}..."); + if (File.Exists(path)) { File.Delete(path); @@ -108,60 +147,134 @@ private static void ExecuteTestAndTearDown(Action action) DeleteFile(Source); } } + #endregion [TestMethod] public void Copy_copies_to_destination() { - var content = BinaryContent; - ExecuteTestAndTearDown(() => { - CreateFile(Source, content); + var content = BinaryContent; - File.Copy(Source, Destination); + CreateFile( + Source, + content); - AssertContentEquals(Source, content); - AssertContentEquals(Destination, content); + File.Copy( + Source, + Destination); + + AssertContentEquals( + Source, + content); + + AssertContentEquals( + Destination, + content); }); ExecuteTestAndTearDown(() => { - CreateFile(Source, content); + var content = BinaryContent; - File.Copy(Source, Destination, overwrite: false); + CreateFile( + Source, + content); - AssertContentEquals(Source, content); - AssertContentEquals(Destination, content); + File.Copy( + Source, + Destination, + overwrite: false); + + AssertContentEquals( + Source, + content); + + AssertContentEquals( + Destination, + content); }); } [TestMethod] public void Copy_overwrites_destination() { - var content = BinaryContent; - ExecuteTestAndTearDown(() => { - CreateFile(Source, content); - CreateFile(Destination, new byte[100]); + var content = BinaryContent; - File.Copy(Source, Destination, overwrite: true); + CreateFile( + Source, + content); - AssertContentEquals(Source, content); - AssertContentEquals(Destination, content); + CreateFile( + Destination, + new byte[100]); + + File.Copy( + Source, + Destination, + overwrite: true); + + AssertContentEquals( + Source, + content); + + AssertContentEquals( + Destination, + content); }); } [TestMethod] public void Copy_throws_if_destination_is_null_or_empty() { - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(Source, null)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(Source, string.Empty)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(Source, null, overwrite: false)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(Source, string.Empty, overwrite: false)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(Source, null, overwrite: true)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(Source, string.Empty, overwrite: true)); + Assert.ThrowsException(typeof(ArgumentNullException), () => + { + File.Copy( + Source, + null); + }); + + Assert.ThrowsException(typeof(ArgumentException), () => + { + File.Copy( + Source, + string.Empty); + }); + + Assert.ThrowsException(typeof(ArgumentNullException), () => + { + File.Copy( + Source, + null, + overwrite: false); + }); + + Assert.ThrowsException(typeof(ArgumentException), () => + { + File.Copy( + Source, + string.Empty, + overwrite: false); + }); + + Assert.ThrowsException(typeof(ArgumentNullException), () => + { + File.Copy( + Source, + null, + overwrite: true); + }); + + Assert.ThrowsException(typeof(ArgumentException), () => + { + File.Copy( + Source, + string.Empty, + overwrite: true); + }); } [TestMethod] @@ -169,22 +282,75 @@ public void Copy_throws_if_destination_exists() { ExecuteTestAndTearDown(() => { - CreateFile(Destination, EmptyContent); - - Assert.ThrowsException(typeof(IOException), () => { File.Copy(Source, Destination); }); - Assert.ThrowsException(typeof(IOException), () => { File.Copy(Source, Destination, overwrite: false); }); + CreateFile( + Destination, + EmptyContent); + + Assert.ThrowsException(typeof(IOException), () => + { + File.Copy( + Source, + Destination); + }); + + Assert.ThrowsException(typeof(IOException), () => + { + File.Copy( + Source, + Destination, + overwrite: false); + }); }); } [TestMethod] public void Copy_throws_if_source_is_null_or_empty() { - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(null, Destination)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(string.Empty, Destination)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(null, Destination, overwrite: false)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(string.Empty, Destination, overwrite: false)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(null, Destination, overwrite: true)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Copy(string.Empty, Destination, overwrite: true)); + Assert.ThrowsException(typeof(ArgumentNullException), () => + { + File.Copy( + null, + Destination); + }); + + Assert.ThrowsException(typeof(ArgumentException), () => + { + File.Copy( + string.Empty, + Destination); + }); + + Assert.ThrowsException(typeof(ArgumentNullException), () => + { + File.Copy( + null, + Destination, + overwrite: false); + }); + + Assert.ThrowsException(typeof(ArgumentException), () => + { + File.Copy( + string.Empty, + Destination, + overwrite: false); + }); + + Assert.ThrowsException(typeof(ArgumentNullException), () => + { + File.Copy( + null, + Destination, + overwrite: true); + }); + + Assert.ThrowsException(typeof(ArgumentException), () => + { + File.Copy( + string.Empty, + Destination, + overwrite: true); + }); } [TestMethod] @@ -192,29 +358,105 @@ public void Create_creates_file() { ExecuteTestAndTearDown(() => { + OutputHelper.WriteLine($"Creating file {Destination} WITHOUT content..."); using var stream = File.Create(Destination); + Console.WriteLine("Checking it file exists..."); AssertFileExists(Destination); - AssertContentEquals(stream, EmptyContent); + + Console.WriteLine("Checking file content..."); + AssertContentEquals( + stream, + EmptyContent); }); ExecuteTestAndTearDown(() => { - CreateFile(Destination, new byte[100]); + OutputHelper.WriteLine($"Creating file: {Destination} WITH content..."); + CreateFile( + Destination, + new byte[100]); + Console.WriteLine("Creating file and truncating it..."); using var stream = File.Create(Destination); + Console.WriteLine("Checking it file exists..."); AssertFileExists(Destination); - AssertContentEquals(stream, EmptyContent); + + Console.WriteLine("Checking file content..."); + AssertContentEquals( + stream, + EmptyContent); }); } + [TestMethod] + public void Create_creates_multiple_files() + { + var testFileNames = new[] + { + $"{Root}{Guid.NewGuid()}.tmp", + $"{Root}{Guid.NewGuid()}.tmp", + $"{Root}{Guid.NewGuid()}.tmp", + $"{Root}{Guid.NewGuid()}.tmp", + $"{Root}{Guid.NewGuid()}.tmp", + $"{Root}{Guid.NewGuid()}.tmp", + }; + + var fileContent = new[] + { + $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}", + $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}", + $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}", + $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}", + $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}", + $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}", + }; + + // delete files if they exist + for (var i = 0; i < testFileNames.Length; i++) + { + var fileName = testFileNames[i]; + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + + // create files, assert they exist and have the right content + for (var i = 0; i < testFileNames.Length; i++) + { + var fileName = testFileNames[i]; + var content = fileContent[i]; + + OutputHelper.WriteLine($"Creating file: {fileName}..."); + File.WriteAllText(fileName, content); + + Console.WriteLine("Checking it file exists..."); + AssertFileExists(fileName); + + Console.WriteLine("Checking file content..."); + AssertContentEquals( + fileName, + content); + } + + // delete files + for (var i = 0; i < testFileNames.Length; i++) + { + var fileName = testFileNames[i]; + File.Delete(fileName); + } + } + [TestMethod] public void Delete_deletes_file() { ExecuteTestAndTearDown(() => { - CreateFile(Source, BinaryContent); + CreateFile( + Source, + BinaryContent); File.Delete(Source); @@ -225,7 +467,7 @@ public void Delete_deletes_file() [TestMethod] public void Exists_returns_false_if_file_does_not_exist() { - Assert.IsFalse(File.Exists($@"I:\file_does_not_exist-{nameof(FileUnitTests)}.pretty_sure")); + Assert.IsFalse(File.Exists($@"{Root}file_does_not_exist-{nameof(FileUnitTests)}.pretty_sure")); } [TestMethod] @@ -262,28 +504,6 @@ public void GetAttributes_throws_if_file_does_not_exist() }); } - [TestMethod] - public void GetLastWriteTime_returns_DateTime() - { - ExecuteTestAndTearDown(() => - { - CreateFile(Source, BinaryContent); - - var actual = File.GetLastWriteTime(Source); - - Assert.IsTrue(actual != default, "Failed to get last write time."); - }); - } - - [TestMethod] - public void GetLastWriteTime_throws_if_path_does_not_exist() - { - ExecuteTestAndTearDown(() => - { - Assert.ThrowsException(typeof(IOException), () => { File.GetLastWriteTime(Source); }); - }); - } - [TestMethod] public void Move_moves_to_destination() { @@ -308,30 +528,56 @@ public void Move_throws_if_destination_exists() CreateFile(Source, BinaryContent); CreateFile(Destination, BinaryContent); - Assert.ThrowsException(typeof(IOException), () => File.Move(Source, Destination)); + Assert.ThrowsException( + typeof(IOException), + () => File.Move( + Source, + Destination)); }); } [TestMethod] public void Move_throws_if_destination_is_null_or_empty() { - Assert.ThrowsException(typeof(ArgumentException), () => File.Move(Source, null)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Move(Source, string.Empty)); + Assert.ThrowsException( + typeof(ArgumentNullException), + () => File.Move( + Source, + null)); + + Assert.ThrowsException( + typeof(ArgumentException), + () => File.Move( + Source, + string.Empty)); } public void Move_throws_if_source_does_not_exist() { ExecuteTestAndTearDown(() => { - Assert.ThrowsException(typeof(IOException), () => File.Move(Source, Destination)); + Assert.ThrowsException( + typeof(IOException), + () => File.Move( + Source, + Destination)); }); } [TestMethod] public void Move_throws_if_source_is_null_or_empty() { - Assert.ThrowsException(typeof(ArgumentException), () => File.Move(null, Destination)); - Assert.ThrowsException(typeof(ArgumentException), () => File.Move(string.Empty, Destination)); + Assert.ThrowsException( + typeof(ArgumentNullException), + () => File.Move( + null, + Destination)); + + Assert.ThrowsException( + typeof(ArgumentException), + () => File.Move( + string.Empty, + Destination)); } [TestMethod] @@ -339,11 +585,15 @@ public void OpenRead_should_open_existing_file() { ExecuteTestAndTearDown(() => { - CreateFile(Source, BinaryContent); + CreateFile( + Source, + BinaryContent); using var actual = File.OpenRead(Source); - AssertContentEquals(actual, BinaryContent); + AssertContentEquals( + actual, + BinaryContent); }); } @@ -352,7 +602,9 @@ public void OpenRead_should_throw_if_file_does_not_exist() { ExecuteTestAndTearDown(() => { - Assert.ThrowsException(typeof(IOException), () => { File.OpenRead(Source); }); + Assert.ThrowsException( + typeof(IOException), + () => { _ = File.OpenRead(Source); }); }); } @@ -361,11 +613,15 @@ public void OpenText_should_open_existing_file() { ExecuteTestAndTearDown(() => { - CreateFile(Source, TextContent); + CreateFile( + Source, + TextContent); using var actual = File.OpenText(Source); - Assert.AreEqual(TextContent, actual.ReadToEnd()); + Assert.AreEqual( + TextContent, + actual.ReadToEnd()); }); } @@ -374,7 +630,9 @@ public void OpenText_should_throw_if_file_does_not_exist() { ExecuteTestAndTearDown(() => { - Assert.ThrowsException(typeof(IOException), () => { File.OpenText(Source); }); + Assert.ThrowsException( + typeof(IOException), + () => { _ = File.OpenText(Source); }); }); } @@ -383,11 +641,15 @@ public void ReadAllBytes_should_read_all_content_from_file() { ExecuteTestAndTearDown(() => { - CreateFile(Source, BinaryContent); + CreateFile( + Source, + BinaryContent); var actual = File.ReadAllBytes(Source); - AssertContentEquals(Source, actual); + AssertContentEquals( + Source, + actual); }); } @@ -396,7 +658,9 @@ public void ReadAllBytes_should_throw_if_file_does_not_exist() { ExecuteTestAndTearDown(() => { - Assert.ThrowsException(typeof(IOException), () => { File.ReadAllBytes(Source); }); + Assert.ThrowsException( + typeof(IOException), + () => { _ = File.ReadAllBytes(Source); }); }); } @@ -405,11 +669,15 @@ public void ReadAllText_should_read_all_content_from_file() { ExecuteTestAndTearDown(() => { - CreateFile(Source, TextContent); + CreateFile( + Source, + TextContent); var actual = File.ReadAllText(Source); - Assert.AreEqual(TextContent, actual); + Assert.AreEqual( + TextContent, + actual); }); } @@ -418,22 +686,34 @@ public void ReadAllText_should_throw_if_file_does_not_exist() { ExecuteTestAndTearDown(() => { - Assert.ThrowsException(typeof(IOException), () => { File.ReadAllText(Source); }); + Assert.ThrowsException( + typeof(IOException), + () => { _ = File.ReadAllText(Source); }); }); } [TestMethod] public void SetAttributes_sets_FileAttributes() { + ////////////////////////////////////////////////////////////////////////////////////////// + // this is failing on ESP32 littlefs because the current API doesn't support attributes // + ////////////////////////////////////////////////////////////////////////////////////////// + ExecuteTestAndTearDown(() => { - CreateFile(Source, BinaryContent); + CreateFile( + Source, + BinaryContent); - File.SetAttributes(Source, FileAttributes.Hidden); + File.SetAttributes( + Source, + FileAttributes.Hidden); var fileAttributes = File.GetAttributes(Source); - Assert.AreEqual(false, fileAttributes.HasFlag(FileAttributes.Hidden), "File does not have hidden attribute"); + Assert.IsTrue( + fileAttributes.HasFlag(FileAttributes.Hidden), + "File does not have hidden attribute"); }); } @@ -442,7 +722,14 @@ public void SetAttributes_throws_if_file_does_not_exist() { ExecuteTestAndTearDown(() => { - Assert.ThrowsException(typeof(IOException), () => { File.SetAttributes(Source, FileAttributes.Hidden); }); + Assert.ThrowsException( + typeof(IOException), + () => + { + File.SetAttributes( + Source, + FileAttributes.Hidden); + }); }); } @@ -451,7 +738,9 @@ public void WriteAllBytes_should_create_file() { ExecuteTestAndTearDown(() => { - File.WriteAllBytes(Source, EmptyContent); + File.WriteAllBytes( + Source, + EmptyContent); AssertFileExists(Source); }); @@ -462,11 +751,17 @@ public void WriteAllBytes_should_overwrite_existing_file() { ExecuteTestAndTearDown(() => { - CreateFile(Source, new byte[100]); + CreateFile( + Source, + new byte[100]); - File.WriteAllBytes(Source, BinaryContent); + File.WriteAllBytes( + Source, + BinaryContent); - AssertContentEquals(Source, BinaryContent); + AssertContentEquals( + Source, + BinaryContent); }); } @@ -475,9 +770,44 @@ public void WriteAllText_should_create_file() { ExecuteTestAndTearDown(() => { - File.WriteAllText(Source, string.Empty); + File.WriteAllText( + Source, + TextContent); + + AssertFileExists(Source); + }); + } + + + [TestMethod] + public void WriteAllTextLargeContent_should_create_file() + { + ExecuteTestAndTearDown(() => + { + var largeContent = new StringBuilder(); + largeContent.Append(TextContent); + largeContent.AppendLine(Guid.NewGuid().ToString()); + largeContent.Append(TextContent); + largeContent.AppendLine(Guid.NewGuid().ToString()); + largeContent.Append(TextContent); + largeContent.AppendLine(Guid.NewGuid().ToString()); + largeContent.Append(TextContent); + largeContent.AppendLine(Guid.NewGuid().ToString()); + largeContent.Append(TextContent); + largeContent.AppendLine(Guid.NewGuid().ToString()); + largeContent.Append(TextContent); + largeContent.AppendLine(Guid.NewGuid().ToString()); + largeContent.Append(TextContent); + + File.WriteAllText( + Source, + largeContent.ToString()); AssertFileExists(Source); + + AssertContentEquals( + Source, + largeContent.ToString()); }); } @@ -486,11 +816,17 @@ public void WriteAllText_should_overwrite_existing_file() { ExecuteTestAndTearDown(() => { - CreateFile(Source, EmptyContent); + CreateFile( + Source, + EmptyContent); - File.WriteAllText(Source, TextContent); + File.WriteAllText( + Source, + TextContent); - AssertContentEquals(Source, TextContent); + AssertContentEquals( + Source, + TextContent); }); } } diff --git a/System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs b/System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs index e7fda43..36a8376 100644 --- a/System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs +++ b/System.IO.FileSystem.UnitTests/PathInternalUnitTests.cs @@ -1,4 +1,9 @@ -using nanoFramework.TestFramework; +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; namespace System.IO.FileSystem.UnitTests { @@ -15,5 +20,26 @@ public void IsValidDriveChar_returns_true() Assert.IsTrue(PathInternal.IsValidDriveChar(test), $"Case: {test}"); } } + + [TestMethod] + [DataRow("folder1/folder2/folder3", "folder1\\folder2\\folder3", "Case: Forward slash")] + [DataRow("folder1\\folder2\\folder3", "folder1\\folder2\\folder3", "Case: Back slash")] + [DataRow("folder1/folder2\\folder3", "folder1\\folder2\\folder3", "Case: Mixed slashes")] + [DataRow("folder1\\..\\folder2\\folder3", "folder2\\folder3", "Case: Navigation commands")] + [DataRow("D:\\FileUnitTests-Destination.test", "D:\\FileUnitTests-Destination.test", "Case: Navigation commands in filename")] + [DataRow("folder1/./folder2/folder3", "folder1\\folder2\\folder3", "Case: Current directory command")] + [DataRow("folder1/../../folder2/folder3", "folder2\\folder3", "Case: Multiple navigation commands")] + [DataRow("folder1/folder2/folder3/..", "folder1\\folder2", "Case: Navigation command at end")] + [DataRow("folder1/folder2/..folder3", "folder1\\folder2\\..folder3", "Case: Navigation command in filename")] + [DataRow("folder1/folder2/.../folder3", "folder1\\folder2\\...\\folder3", "Case: Triple dot in path")] + [DataRow("D:\\", "D:\\", "Case: Handle root paths")] + public void NormalizeDirectorySeparators_Returns_Correct_Path( + string path, + string expected, + string caseName) + { + var actual = PathInternal.NormalizeDirectorySeparators(path); + Assert.AreEqual(expected, actual, caseName); + } } } diff --git a/System.IO.FileSystem.UnitTests/PathUnitTests.cs b/System.IO.FileSystem.UnitTests/PathUnitTests.cs index ca23208..63403b4 100644 --- a/System.IO.FileSystem.UnitTests/PathUnitTests.cs +++ b/System.IO.FileSystem.UnitTests/PathUnitTests.cs @@ -1,19 +1,28 @@ -// +// // Copyright (c) .NET Foundation and Contributors // See LICENSE file in the project root for full license information. +// using nanoFramework.TestFramework; namespace System.IO.FileSystem.UnitTests { [TestClass] - internal class PathUnitTests + public class PathUnitTests : FileSystemUnitTestsBase { + [Setup] + public void Setup() + { + Assert.SkipTest("These test will only run on real hardware. Comment out this line if you are testing on real hardware."); + + RemovableDrivesHelper(); + } + [TestMethod] public void ChangeExtension_adds_extension() { - const string path = @"I:\file"; - const string expect = @"I:\file.new"; + string path = @$"{Root}file"; + string expect = @$"{Root}file.new"; Assert.AreEqual(expect, Path.ChangeExtension(path, "new")); Assert.AreEqual(expect, Path.ChangeExtension(path, ".new")); @@ -22,8 +31,8 @@ public void ChangeExtension_adds_extension() [TestMethod] public void ChangeExtension_changes_extension() { - const string path = @"I:\file.old"; - const string expect = @"I:\file.new"; + string path = @$"{Root}file.old"; + string expect = @$"{Root}file.new"; Assert.AreEqual(expect, Path.ChangeExtension(path, "new")); Assert.AreEqual(expect, Path.ChangeExtension(path, ".new")); @@ -32,8 +41,8 @@ public void ChangeExtension_changes_extension() [TestMethod] public void ChangeExtension_removes_extension() { - const string path = @"I:\file.old"; - const string expect = @"I:\file"; + string path = @$"{Root}file.old"; + string expect = @$"{Root}file"; Assert.AreEqual(expect, Path.ChangeExtension(path, null)); } @@ -64,17 +73,17 @@ public void Combine_returns_path1_if_path2_is_empty_string() [TestMethod] public void Combine_combines_paths() { - var expect = @"I:\Path1\Path2\File.ext"; + var expect = @$"{Root}Path1\Path2\File.ext"; - Assert.AreEqual(expect, Path.Combine(@"I:\Path1", @"Path2\File.ext")); - Assert.AreEqual(expect, Path.Combine(@"I:\Path1\", @"Path2\File.ext")); + Assert.AreEqual(expect, Path.Combine(@$"{Root}Path1", @"Path2\File.ext")); + Assert.AreEqual(expect, Path.Combine(@$"{Root}Path1\", @"Path2\File.ext")); } [TestMethod] public void Combine_returns_path2_if_it_is_an_absolute_path() { - var path1 = @"I:\Directory"; - var path2 = @"I:\Absolute\Path"; + var path1 = @$"{Root}Directory"; + var path2 = @$"{Root}Absolute\Path"; var actual = Path.Combine(path1, path2); @@ -101,14 +110,14 @@ public void Combine_throws_if_path1_is_null() [TestMethod] public void Combine_throws_if_path2_is_null() { - Assert.ThrowsException(typeof(ArgumentNullException), () => { Path.Combine(@"I:\Directory", null); }); + Assert.ThrowsException(typeof(ArgumentNullException), () => { Path.Combine(@$"{Root}Directory", null); }); } [TestMethod] public void GetDirectoryName_returns_directory() { - var tests = new[] { @"I:\directory", @"I:\directory\", @"I:\directory\file.ext" }; - var answers = new[] { @"I:\", @"I:\directory", @"I:\directory" }; + var tests = new[] { @$"{Root}directory", @$"{Root}directory\", @$"{Root}directory\file.ext" }; + var answers = new[] { @$"{Root}", @$"{Root}directory", @$"{Root}directory" }; for (var i = 0; i < tests.Length; i++) { @@ -120,20 +129,22 @@ public void GetDirectoryName_returns_directory() } [TestMethod] - public void GetDirectoryName_returns_directory_UNC_paths() + [DataRow(@"\\server\share\", @"\\server\share", "server UNC")] + [DataRow(@"\\server\share\file.ext", @"\\server\share", "file UNC")] + public void GetDirectoryName_returns_directory_UNC_paths( + string test, + string expected, + string caseName) { - var tests = new[] { @"\\server\share\", @"\\server\share\file.ext" }; - var answers = new[] { @"\\server\share", @"\\server\share" }; - - for (var i = 0; i < tests.Length; i++) - { - var test = tests[i]; - var expected = answers[i]; + Assert.SkipTest("UNC paths are not supported in the default build"); - Assert.AreEqual(expected, Path.GetDirectoryName(test), $"Case: {test}"); - } + Assert.AreEqual( + expected, + Path.GetDirectoryName(test), + $"Case: {caseName}"); } + [TestMethod] public void GetDirectoryName_returns_null() { @@ -179,15 +190,17 @@ public void GetExtension_returns_extension() var expect = ".ext"; Assert.AreEqual(expect, Path.GetExtension(file)); - Assert.AreEqual(expect, Path.GetExtension($"I:{file}")); - Assert.AreEqual(expect, Path.GetExtension(@$"I:\{file}")); - Assert.AreEqual(expect, Path.GetExtension(@$"I:\directory\{file}")); + Assert.AreEqual(expect, Path.GetExtension($"{Root}{file}")); + Assert.AreEqual(expect, Path.GetExtension(@$"{Root}{file}")); + Assert.AreEqual(expect, Path.GetExtension(@$"{Root}directory\{file}")); Assert.AreEqual(expect, Path.GetExtension(@$"\{file}")); } [TestMethod] public void GetExtension_returns_extension_UNC_paths() { + Assert.SkipTest("UNC paths are not supported in the default build"); + var file = "file.ext"; var expect = ".ext"; @@ -204,22 +217,24 @@ public void GetExtension_returns_null() [TestMethod] public void GetFilename_returns_empty_string() { - Assert.AreEqual(string.Empty, Path.GetFileName("I:")); - Assert.AreEqual(string.Empty, Path.GetFileName(@"I:\")); + Assert.AreEqual(string.Empty, Path.GetFileName($"{Root.Substring(0, 2)}")); + Assert.AreEqual(string.Empty, Path.GetFileName(@$"{Root}")); } [TestMethod] public void GetFilename_returns_filename_without_extension() { - Assert.AreEqual("file", Path.GetFileName(@"I:\directory\file")); - Assert.AreEqual("file.ext", Path.GetFileName(@"I:\directory\file.ext")); - Assert.AreEqual("file", Path.GetFileName(@"I:\file")); - Assert.AreEqual("file.ext", Path.GetFileName(@"I:\file.ext")); + Assert.AreEqual("file", Path.GetFileName(@$"{Root}directory\file")); + Assert.AreEqual("file.ext", Path.GetFileName(@$"{Root}directory\file.ext")); + Assert.AreEqual("file", Path.GetFileName(@$"{Root}file")); + Assert.AreEqual("file.ext", Path.GetFileName(@$"{Root}file.ext")); } [TestMethod] public void GetFilename_returns_filename_without_extension_UNC_paths() { + Assert.SkipTest("UNC paths are not supported in the default build"); + Assert.AreEqual("file", Path.GetFileName(@"\\server\share\directory\file")); Assert.AreEqual("file.ext", Path.GetFileName(@"\\server\share\directory\file.ext")); } @@ -233,22 +248,24 @@ public void GetFilename_returns_null() [TestMethod] public void GetFilenameWithoutExtension_returns_empty_string() { - Assert.AreEqual(string.Empty, Path.GetFileNameWithoutExtension("I:")); - Assert.AreEqual(string.Empty, Path.GetFileNameWithoutExtension(@"I:\")); + Assert.AreEqual(string.Empty, Path.GetFileNameWithoutExtension($"{Root.Substring(0, 2)}")); + Assert.AreEqual(string.Empty, Path.GetFileNameWithoutExtension(@$"{Root}")); } [TestMethod] public void GetFilenameWithoutExtension_returns_filename_without_extension() { - Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\directory\file")); - Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\directory\file.ext")); - Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\file")); - Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"I:\file.ext")); + Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@$"{Root}directory\file")); + Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@$"{Root}directory\file.ext")); + Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@$"{Root}file")); + Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@$"{Root}file.ext")); } [TestMethod] public void GetFilenameWithoutExtension_returns_filename_without_extension_UNC_paths() { + Assert.SkipTest("UNC paths are not supported in the default build"); + Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"\\server\share\directory\file")); Assert.AreEqual("file", Path.GetFileNameWithoutExtension(@"\\server\share\directory\file.ext")); } @@ -280,10 +297,10 @@ public void GetPathRoot_returns_root() { var tests = new[] { - "I:", @"I:\directory\file", @"I:\directory\file.ext", @"I:\file", @"I:\file.ext" + $"{Root.Substring(0, 2)}", @$"{Root}directory\file", @$"{Root}directory\file.ext", @$"{Root}file", @$"{Root}file.ext" }; - var answers = new[] { "I:", @"I:\", @"I:\", @"I:\", @"I:\" }; + var answers = new[] { $"{Root.Substring(0, 2)}", @$"{Root}", @$"{Root}", @$"{Root}", @$"{Root}" }; for (var i = 0; i < tests.Length; i++) { @@ -315,27 +332,37 @@ public void GetPathRoot_returns_root_UNC_paths() } } - [DataRow(@"\dir1\dir2\file.ext")] - [DataRow(@"\dir1\dir2\file.ext")] - [DataRow(@"\dir1\..\dir2\file.ext")] - [DataRow(@"\dir1\..\dir2\..\dir3\file.ext")] + [DataRow(@"\dir1\..\..\dir2\", @"dir2\", false)] + [DataRow(@"\dir1\..\..\dir2", @"dir2", false)] + [DataRow(@"dir1\dir2\", @"dir1\dir2\", true)] + [DataRow(@"dir1\dir2", @"dir1\dir2", true)] + [DataRow(@"\dir1\dir2\", @"\dir1\dir2\", false)] + [DataRow(@"\dir1\dir2\file.ext", @"\dir1\dir2\file.ext", false)] + [DataRow(@"\dir1\..\dir2\file.ext", @"dir2\file.ext", false)] + [DataRow(@"dir1\..\dir2\file.ext", @"dir2\file.ext", true)] + [DataRow(@"\dir1\..\dir2\..\dir3\file.ext", @"dir3\file.ext", false)] [TestMethod] - public void GetFullPathWithFiles(string pathToTest) + public void TestGetFullPath( + string pathToTest, + string expectedPath, + bool isRooted) { - string fullPath = Path.GetFullPath(pathToTest); - Assert.AreEqual(pathToTest, fullPath); - } + string resultPath = Path.GetFullPath(pathToTest); - [DataRow(@"\dir1\..\..\dir2\", @"\dir1\..\..\dir2\")] - [DataRow(@"\dir1\..\..\dir2", @"\dir1\..\..\dir2")] - [DataRow(@"dir1\dir2\", @"dir1\dir2\")] - [DataRow(@"dir1\dir2", @"dir1\dir2")] - [DataRow(@"\dir1\dir2\", @"\dir1\dir2\")] - [TestMethod] - public void GetFullPathWithDirectories(string pathToTest, string expectedPath) - { - string fullPath = Path.GetFullPath(pathToTest); - Assert.AreEqual(expectedPath, fullPath); + if (isRooted) + { + Assert.AreEqual( + $"{Root}{expectedPath}", + resultPath, + message: $"Case: {expectedPath}"); + } + else + { + Assert.AreEqual( + expectedPath, + resultPath, + $"Case: {expectedPath}"); + } } [TestMethod] @@ -343,7 +370,7 @@ public void HasExtension_returns_false() { var tests = new[] { - "file", @"\file.", @"\", "/", "I:", @"I:\", @"I:\directory\" + "file", @"\file.", @"\", "/", "I:", @"{Root}", @"{Root}directory\" }; for (var i = 0; i < tests.Length; i++) @@ -356,6 +383,8 @@ public void HasExtension_returns_false() [TestMethod] public void HasExtension_returns_false_UNC_paths() { + Assert.SkipTest("UNC paths are not supported in the default build"); + var tests = new[] { @"\\server\share\file.", @"\\server\share\directory\file" @@ -373,7 +402,7 @@ public void HasExtension_returns_true() { var tests = new[] { - "file.ext", @"\file.ext", "/file.ext", "I:file.ext", @"I:\file.ext", @"I:\directory\file.ext" + "file.ext", @"\file.ext", "/file.ext", "I:file.ext", @"{Root}file.ext", @"{Root}directory\file.ext" }; for (var i = 0; i < tests.Length; i++) @@ -386,6 +415,8 @@ public void HasExtension_returns_true() [TestMethod] public void HasExtension_returns_true_UNC_paths() { + Assert.SkipTest("UNC paths are not supported in the default build"); + var tests = new[] { @"\\server\share\file.ext", @"\\server\share\directory\file.ext" @@ -403,7 +434,7 @@ public void IsPathRooted_returns_true() { var tests = new[] { - @"\", "/", "I:", @"I:\", @"I:\file.ext", @"I:\directory\file.ext" + @"\", "/", $"{Root.Substring(0, 2)}", @$"{Root}", @$"{Root}file.ext", @$"{Root}directory\file.ext" }; for (var i = 0; i < tests.Length; i++) @@ -416,6 +447,8 @@ public void IsPathRooted_returns_true() [TestMethod] public void IsPathRooted_returns_true_UNC_paths() { + Assert.SkipTest("UNC paths are not supported in the default build"); + var tests = new[] { @"\\server\share", @"\\server\share\file.ext", @"\\server\share\directory\file.ext" diff --git a/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj b/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj index 8a588d7..234b245 100644 --- a/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj +++ b/System.IO.FileSystem.UnitTests/System.IO.FileSystem.UnitTests.nfproj @@ -37,6 +37,8 @@ $(MSBuildProjectDirectory)\nano.runsettings + + @@ -59,17 +61,14 @@ ..\packages\nanoFramework.System.Text.1.2.54\lib\nanoFramework.System.Text.dll - + ..\packages\nanoFramework.TestFramework.2.1.107\lib\nanoFramework.TestFramework.dll - True - + ..\packages\nanoFramework.TestFramework.2.1.107\lib\nanoFramework.UnitTestLauncher.exe - True ..\packages\nanoFramework.System.IO.Streams.1.1.59\lib\System.IO.Streams.dll - True diff --git a/System.IO.FileSystem/Directory.cs b/System.IO.FileSystem/Directory.cs index 2cd3246..a82ac70 100644 --- a/System.IO.FileSystem/Directory.cs +++ b/System.IO.FileSystem/Directory.cs @@ -3,150 +3,345 @@ // See LICENSE file in the project root for full license information. // +using System.Collections; using System.Runtime.CompilerServices; namespace System.IO { /// - /// Class for managing directories + /// Exposes static methods for creating, moving, and enumerating through directories and subdirectories. This class cannot be inherited. /// public static class Directory { - #region Static Methods - /// - /// Determines a list of available logical drives. + /// Creates all directories and subdirectories in the specified path unless they already exist. /// - /// String[] of available drives, ex. "D:\\" - [Obsolete("Use DriveInfo.GetDrives() instead.")] - public static string[] GetLogicalDrives() + /// The directory to create. + /// An object that represents the directory at the specified path. This object is returned regardless of whether a directory at the specified path already exists. + /// The directory specified by path is a file. + public static DirectoryInfo CreateDirectory(string path) { - return GetLogicalDrivesNative(); + // path validation happening in the call + path = Path.GetFullPath(path); + + // According to the .NET API, Directory.CreateDirectory on an existing directory returns the DirectoryInfo object for the existing directory. + NativeIO.CreateDirectory(path); + + return new DirectoryInfo(path); } /// - /// Creates directory with the provided path. + /// Deletes an empty directory from a specified path. /// - /// Path and name of the directory to create. - /// Path for creating the folder doesn't exist. This method does not create directories recursively. - public static void CreateDirectory(string path) + /// The name of the empty directory to remove. This directory must be writable and empty. + /// The directory specified by path is not empty. + public static void Delete(string path) { - CreateNative(path); + Delete( + path, + false); } + /// - /// Deletes directory from storage. + /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. /// - /// Path to the directory to be removed. - /// Parameter to be implemented. - /// This method will throw DirectoryNotEmpty exception if folder is not empty. - public static void Delete(string path, bool recursive = false) + /// The name of the directory to remove. + /// to remove directories, subdirectories, and files in path; otherwise, . + /// A file with the same name and location specified by path exists. -or- The directory specified by path is read-only, or recursive is false and path is not an empty directory. -or- The directory is the application's current working directory. -or- The directory contains a read-only file. -or- The directory is being used by another process. + /// The platform or storage file system may not support recursive deletion of directories. The default value for recursive is . In this case an exception will be thrown. + public static void Delete( + string path, + bool recursive) { - DeleteNative(path); + // path validation happening in the call + path = Path.GetFullPath(path); + + object record = FileSystemManager.LockDirectory(path); + + try + { + uint attributes = NativeIO.GetAttributes(path); + + if (attributes == NativeIO.EmptyAttribute) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); + } + + if (((attributes & (uint)FileAttributes.Directory) == 0) || + ((attributes & (uint)FileAttributes.ReadOnly) != 0)) + { + // it's readonly or not a directory + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); + } + + // make sure it is indeed a directory (and not a file) + if (!Exists(path)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); + } + + NativeIO.Delete( + path, + recursive); + } + finally + { + // regardless of what happened, we need to release the directory when we're done + FileSystemManager.UnlockDirectory(record); + } } /// - /// Determines whether the specified directory exists. + /// Determines whether the given path refers to an existing directory on disk. /// - /// Path to the directory. - /// True if directory under given path exists, otherwise it returns false. + /// The path to test. + /// if refers to an existing directory; if the directory does not exist or an error occurs when trying to determine if the specified directory exists. /// Path must be defined. /// Invalid drive or path to the parent folder doesn't exist. public static bool Exists(string path) { - return ExistsNative(path); + // path validation happening in the call + path = Path.GetFullPath(path); + + // is this the absolute root? this always exists. + if (path == NativeIO.FSRoot) + { + return true; + } + + uint attributes = NativeIO.GetAttributes(path); + + if (attributes == NativeIO.EmptyAttribute) + { + // this means not found + return false; + } + + if ((((FileAttributes)attributes) + & FileAttributes.Directory) == FileAttributes.Directory) + { + // this is a directory + return true; + } + + return false; } /// - /// Moves directory from specified path to a new location. + /// Gets the current working directory of the application. /// - /// Name of directory to move. Absolute path. - /// New path and name for the directory. - /// Source directory not existing or destination folder already existing. - public static void Move(string sourcePath, string destinationPath) + /// + public static string GetCurrentDirectory() { - MoveNative(sourcePath, destinationPath); + return FileSystemManager.CurrentDirectory; } /// - /// List files from the specified folder. + /// Returns the names of files (including their paths) in the specified directory. /// - /// Path to the directory to list files from. + /// The relative or absolute path to the directory to search. This string is not case-sensitive. /// - /// When this method completes successfully, it returns a array of paths of the files in the given folder. + /// An array of the full names (including paths) for the files in the specified directory, or an empty array if no files are found. /// /// Logical drive or a directory under given path does not exist. public static string[] GetFiles(string path) { - return GetFilesNative(path); + // if path doesn't end with a separator, add one + if (!path.EndsWith(Path.DirectorySeparatorChar.ToString()) + && !path.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + path += Path.DirectorySeparatorChar; + } + + return GetChildren( + path, + false); } /// - /// List directories from the specified folder. + /// Returns the names of subdirectories (including their paths) in the specified directory. /// - /// + /// The relative or absolute path to the directory to search. This string is not case-sensitive. /// - /// When this method completes successfully, it returns an array of absolute paths to the subfolders in the specified directory. + /// An array of the full names (including paths) of subdirectories in the specified path, or an empty array if no directories are found. /// /// Logical drive or a directory under given path does not exist. public static string[] GetDirectories(string path) { - return GetDirectoriesNative(path); + return GetChildren( + path, + true); } /// - /// Determines the time of the last write/modification to directory under given path. + /// Moves a file or a directory and its contents to a new location. /// - /// - /// Time of the last write/modification. - /// Logical drive or a directory under given path does not exist. - public static DateTime GetLastWriteTime(string path) + /// The path of the file or directory to move. + /// The path to the new location for or its contents. If is a file, then must also be a file name. + /// n attempt was made to move a directory to a different volume. -or- destDirName already exists. See the note in the Remarks section. -or- The source directory does not exist. -or- The source or destination directory name is . -or- The and parameters refer to the same file or directory. -or- The directory or a file within it is being used by another process. + public static void Move( + string sourceDirName, + string destDirName) { - return GetLastWriteTimeNative(path); + // sourceDirName and destDirName validation happening in the call + sourceDirName = Path.GetFullPath(sourceDirName); + destDirName = Path.GetFullPath(destDirName); + + bool tryCopyAndDelete = false; + object srcRecord = FileSystemManager.AddToOpenList(sourceDirName); + + try + { + // make sure is actually a directory + if (!Exists(sourceDirName)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); + } + + // If Move() returns false, we'll try doing copy and delete to accomplish the move + tryCopyAndDelete = !NativeIO.Move(sourceDirName, destDirName); + } + finally + { + FileSystemManager.RemoveFromOpenList(srcRecord); + } + + if (tryCopyAndDelete) + { + RecursiveCopyAndDelete(sourceDirName, destDirName); + } } - #endregion + /// + /// Sets the application's current working directory to the specified directory. + /// + /// The path to which the current working directory is set. + /// An I/O error occurred. + public static void SetCurrentDirectory(string path) + { + // path validation happening in the call + path = Path.GetFullPath(path); - #region Stubs (Native Calls) + // lock the directory for read-access first, to ensure path won't get deleted + object record = FileSystemManager.AddToOpenListForRead(path); - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool ExistsNative(string path); + try + { + if (!Exists(path)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void MoveNative(string pathSrc, string pathDest); + // put the lock on path. (also read-access) + FileSystemManager.SetCurrentDirectory(path); + } + finally + { + // take lock off + FileSystemManager.RemoveFromOpenList(record); + } + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void DeleteNative(string path); + private static string[] GetChildren( + string path, + bool directoryOnly) + { + // path validation happening in the call + path = Path.GetFullPath(path); - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void CreateNative(string path); + if (!Exists(path)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string[] GetFilesNative(string path); + return NativeGetChildren( + path, + directoryOnly); + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string[] GetDirectoriesNative(string path); + private static void RecursiveCopyAndDelete(string sourceDirName, + string destDirName) + { + string[] files; + int filesCount, i; - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern string[] GetLogicalDrivesNative(); + // make sure no other thread/process can modify it (for example, delete the directory and create a file of the same name) while we're moving + object recordSrc = FileSystemManager.AddToOpenList(sourceDirName); + + try + { + // check that ake sure sourceDir is actually a directory + if (!Exists(sourceDirName)) + { + throw new IOException( + "", + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); + } + + // make sure destDir does not yet exist + if (Exists(destDirName)) + { + throw new IOException( + "", + (int)IOException.IOExceptionErrorCode.PathAlreadyExists); + } + + // create it + NativeIO.CreateDirectory(destDirName); + + // get all files in sourceDir ... + files = GetFiles(sourceDirName); + filesCount = files.Length; + + // ... and copy them to destDir + for (i = 0; i < filesCount; i++) + { + File.Copy( + files[i], + Path.Combine(destDirName, Path.GetFileName(files[i])), + false, + true); + } + + // now get all directories in sourceDir ... + files = GetDirectories(sourceDirName); + filesCount = files.Length; + + // ... and copy them to destDir + for (i = 0; i < filesCount; i++) + { + RecursiveCopyAndDelete( + files[i], + Path.Combine(destDirName, Path.GetFileName(files[i]))); + } + + // finally, delete sourceDir + NativeIO.Delete( + sourceDirName, + true); + } + finally + { + FileSystemManager.RemoveFromOpenList(recordSrc); + } + } + + #region native calls - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] [MethodImpl(MethodImplOptions.InternalCall)] - private static extern DateTime GetLastWriteTimeNative(string path); + private static extern string[] NativeGetChildren(string path, bool directoryOnly); #endregion } diff --git a/System.IO.FileSystem/DirectoryInfo.cs b/System.IO.FileSystem/DirectoryInfo.cs new file mode 100644 index 0000000..75a40c4 --- /dev/null +++ b/System.IO.FileSystem/DirectoryInfo.cs @@ -0,0 +1,197 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +namespace System.IO +{ + /// + /// Exposes instance methods for creating, moving, and enumerating through directories and subdirectories. This class cannot be inherited. + /// + public sealed class DirectoryInfo : FileSystemInfo + { + /// + /// Gets the name of this instance. + /// + public override string Name + { + get + { + return Path.GetFileName(_fullPath); + } + } + + /// + public override bool Exists + { + get + { + return Directory.Exists(_fullPath); + } + } + + /// + /// Gets the root portion of the directory. + /// + /// An object that represents the root of the directory. + public DirectoryInfo Root + { + get + { + return new DirectoryInfo(Path.GetPathRoot(_fullPath)); + } + } + + /// + /// Gets the parent directory of a specified subdirectory. + /// + public DirectoryInfo Parent + { + get + { + // FullPath might be either "c:\bar" or "c:\bar\". Handle + // those cases, as well as avoiding mangling "c:\". + string s = _fullPath; + + if (s.Length > 3 + && s.EndsWith(PathInternal.DirectorySeparatorCharAsString)) + { + s = _fullPath.Substring(0, _fullPath.Length - 1); + } + + string parentDirPath = Path.GetDirectoryName(s); + + if (parentDirPath == null) + { + return null; + } + + return new DirectoryInfo(parentDirPath); + } + } + + /// + /// Initializes a new instance of the class on the specified path. + /// + /// + public DirectoryInfo(string path) + { + // path validation happening in the call + _fullPath = Path.GetFullPath(path); + } + + /// + /// Creates a directory. + /// + public void Create() + { + Directory.CreateDirectory(_fullPath); + } + + /// + /// Creates a subdirectory or subdirectories on the specified path. The specified path can be relative to this instance of the class. + /// + /// + /// + public DirectoryInfo CreateSubdirectory(string path) + { + // path validatation happening in the call + string subDirPath = Path.Combine( + _fullPath, + path); + + // This will also ensure "path" is valid. + subDirPath = Path.GetFullPath(subDirPath); + + return Directory.CreateDirectory(subDirPath); + } + + /// + /// Returns a file list from the current directory. + /// + /// + /// An array of type . + /// + /// Logical drive or a directory under given path does not exist. + public FileInfo[] GetFiles() + { + string[] fileNames = Directory.GetFiles(_fullPath); + + FileInfo[] files = new FileInfo[fileNames.Length]; + + for (int i = 0; i < fileNames.Length; i++) + { + files[i] = new FileInfo(fileNames[i]); + } + + return files; + } + + /// + /// Returns the subdirectories of the current directory. + /// + /// An array of objects. + public DirectoryInfo[] GetDirectories() + { + // searchPattern validation happening in the call + string[] dirNames = Directory.GetDirectories(_fullPath); + + DirectoryInfo[] dirs = new DirectoryInfo[dirNames.Length]; + + for (int i = 0; i < dirNames.Length; i++) + { + dirs[i] = new DirectoryInfo(dirNames[i]); + } + + return dirs; + } + + /// + /// Moves a instance and its contents to a new path. + /// + /// The name and path to which to move this directory. The destination cannot be another disk volume or a directory with the identical name. It can be an existing directory to which you want to add this directory as a subdirectory. + /// An attempt was made to move a directory to a different volume. -or- already exists. -or- The source directory does not exist. -or- The source or destination directory name is ." + /// is ." + public void MoveTo(string destDirName) + { + // destDirName validation happening in the call + Directory.Move( + _fullPath, + destDirName); + } + + /// + public override void Delete() + { + Directory.Delete(_fullPath); + } + + /// + /// Deletes this instance of a , specifying whether to delete subdirectories and files. + /// + /// to delete this directory, its subdirectories, and all files; otherwise, . + public void Delete(bool recursive) + { + Directory.Delete( + _fullPath, + recursive); + } + + /// + protected override void HandleRefreshError() + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); + } + + /// + /// Returns the original path that was passed to the constructor. Use the or properties for the full path or file/directory name instead of this method. + /// + /// The original path that was passed by the user. + public override string ToString() + { + return _fullPath; + } + } +} diff --git a/System.IO.FileSystem/DriveInfo.cs b/System.IO.FileSystem/DriveInfo.cs index 95e8ea6..2637697 100644 --- a/System.IO.FileSystem/DriveInfo.cs +++ b/System.IO.FileSystem/DriveInfo.cs @@ -16,6 +16,7 @@ public sealed class DriveInfo private DriveType _driveType; private string _name; private long _totalSize; + internal uint _volumeIndex; /// /// Gets the drive type, such as removable, fixed or RAM. @@ -24,7 +25,7 @@ public sealed class DriveInfo /// One of the enumeration values that specifies a drive type. /// public DriveType DriveType { get => _driveType; set => _driveType = value; } - + /// /// Gets the name of a drive, such as C:\. /// @@ -78,16 +79,70 @@ public static DriveInfo[] GetDrives() /// Formats the specified drive. /// *** NOTE THAT THIS OPERATION IS NOT REVERSIBLE ***. /// - /// - /// does not refer to a valid drive. + /// File system to use for the format operation. + /// A parameter to pass to the format operation. /// Thrown when the target doesn't have support for performing the format operation on the specified drive. /// Thrown when the operation fails. /// /// This method is not reversible. Once the drive is formatted, all data on the drive is lost. /// This method is implemented in the .NET nanoFramework API but it is not supported on all target devices nor on all file systems. /// - [MethodImpl(MethodImplOptions.InternalCall)] - public static extern void Format(string driveName); + public void Format( + string fileSystem, + uint parameter) + { + bool needToRestoreCurrentDir = FileSystemManager.CurrentDirectory == Name; + + if (FileSystemManager.IsInDirectory(FileSystemManager.CurrentDirectory, Name)) + { + FileSystemManager.SetCurrentDirectory(NativeIO.FSRoot); + } + + FileSystemManager.ForceRemoveRootname(Name); + + object record = FileSystemManager.LockDirectory(Name); + + try + { + NativeIO.Format( + Name, + fileSystem, + parameter); + + Refresh(); + } + finally + { + FileSystemManager.UnlockDirectory(record); + } + + if (needToRestoreCurrentDir) + { + FileSystemManager.SetCurrentDirectory(Name); + } + } + + /// + /// Refreshes the information about the drive. + /// + [MethodImpl(MethodImplOptions.InternalCall)] + public extern void Refresh(); + + /// + /// Retrieves the names of the file systems available on the connected device. + /// + /// An array of strings that represent the names of the file systems available on the connected device. + [MethodImpl(MethodImplOptions.InternalCall)] + public extern static string[] GetFileSystems(); + + /// + /// Tries to mount all removable volumes. + /// + /// + /// This method is implemented in the .NET nanoFramework API but it is not supported on all target devices. + /// + [MethodImpl(MethodImplOptions.InternalCall)] + public static extern void MountRemovableVolumes(); #region Native Calls @@ -95,6 +150,10 @@ public static DriveInfo[] GetDrives() [MethodImpl(MethodImplOptions.InternalCall)] private extern void DriveInfoNative(string driveName); + [DebuggerNonUserCode] + [MethodImpl(MethodImplOptions.InternalCall)] + internal extern DriveInfo(uint volumeIndex); + [DebuggerNonUserCode] [MethodImpl(MethodImplOptions.InternalCall)] private static extern DriveInfo[] GetDrivesNative(); diff --git a/System.IO.FileSystem/File.cs b/System.IO.FileSystem/File.cs index b2ba13a..a31ca64 100644 --- a/System.IO.FileSystem/File.cs +++ b/System.IO.FileSystem/File.cs @@ -3,29 +3,68 @@ // See LICENSE file in the project root for full license information. // -using System.Runtime.CompilerServices; using System.Text; namespace System.IO { /// - /// Class for creating FileStream objects, and some basic file management - /// routines such as Delete, etc. + /// Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of objects. /// public static class File { private const int ChunkSize = 2048; private static readonly byte[] EmptyBytes = new byte[0]; + /// + /// Creates a that appends UTF-8 encoded text to an existing file, or to a new file if the specified file does not exist. + /// + /// The path to the file to append to. + /// A stream writer that appends UTF-8 encoded text to the specified file or to a new file. + public static StreamWriter AppendText(string path) + { + // path validation happening in the call + path = Path.GetFullPath(path); + + return new StreamWriter(OpenWrite(path)); + } + + /// + /// Opens a file, appends the specified string to the file, and then closes the file. If the file does not exist, this method creates a file, writes the specified string to the file, then closes the file. + /// + /// The file to append the specified string to. + /// The string to append to the file. + public static void AppendAllText( + string path, + string contents) + { + // path validation happening in the call + path = Path.GetFullPath(path); + + using var stream = new FileStream( + path, + FileMode.Append, + FileAccess.Write); + + using var streamWriter = new StreamWriter(stream); + + streamWriter.Write(contents); + } + /// /// Copies an existing file to a new file. Overwriting a file of the same name is not allowed. /// /// The file to copy. /// The name of the destination file. This cannot be a directory or an existing file. - /// or is null or empty. - public static void Copy(string sourceFileName, string destFileName) + /// or is or empty. + public static void Copy( + string sourceFileName, + string destFileName) { - Copy(sourceFileName, destFileName, overwrite: false); + Copy( + sourceFileName, + destFileName, + false, + false); } /// @@ -34,51 +73,45 @@ public static void Copy(string sourceFileName, string destFileName) /// The file to copy. /// The name of the destination file. This cannot be a directory. /// if the destination file can be overwritten; otherwise, . - /// or is or empty. - - public static void Copy(string sourceFileName, string destFileName, bool overwrite) + public static void Copy( + string sourceFileName, + string destFileName, + bool overwrite) { - if (string.IsNullOrEmpty(sourceFileName)) - { - throw new ArgumentException(); - } - - if (string.IsNullOrEmpty(destFileName)) - { - throw new ArgumentException(); - } - - if (sourceFileName == destFileName) - { - return; - } - - var destMode = overwrite ? FileMode.Create : FileMode.CreateNew; - - using var sourceStream = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read); - using var destStream = new FileStream(destFileName, destMode, FileAccess.Write); - - var buffer = new byte[ChunkSize]; - var bytesRead = 0; - - while ((bytesRead = sourceStream.Read(buffer, 0, ChunkSize)) > 0) - { - destStream.Write(buffer, 0, bytesRead); - } - - // Copy the attributes too - SetAttributes(destFileName, GetAttributes(sourceFileName)); + Copy( + sourceFileName, + destFileName, + overwrite, + false); } /// - /// Creates or overwrites a file in the specified path. + /// Creates, or truncates and overwrites, a file in the specified path. /// /// The path and name of the file to create. - public static FileStream Create(string path) - { - return new FileStream(path, FileMode.Create, FileAccess.ReadWrite); - } + /// A that provides read/write access to the file specified in . + public static FileStream Create(string path) => new FileStream( + path, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, + NativeFileStream.BufferSizeDefault); + + /// + /// Creates or opens a file for writing UTF-8 encoded text. + /// + /// The path and name of the file to create. + /// The number of bytes buffered for reads and writes to the file. + /// A that provides read/write access to the file specified in . + public static FileStream Create( + string path, + int bufferSize) => new FileStream( + path, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, + bufferSize); /// /// Deletes the specified file. @@ -88,53 +121,50 @@ public static FileStream Create(string path) /// Directory is not found or is read-only or a directory. public static void Delete(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException(); - } + // path validation happening in the call + path = Path.GetFullPath(path); + string folderPath = Path.GetDirectoryName(path); + + // make sure no one else has the file opened, and no one else can modify it when we're deleting + object record = FileSystemManager.AddToOpenList(path); try { - byte attributes; - var directoryName = Path.GetDirectoryName(path); + uint attributes = NativeIO.GetAttributes(folderPath); - // Only check folder if its not the Root - if (directoryName != Path.GetPathRoot(path)) + // in case the folder does not exist or is invalid we throw DirNotFound Exception + if (attributes == NativeIO.EmptyAttribute) { - attributes = GetAttributesNative(directoryName); - - // Check if Directory existing - if (attributes == 0xFF) - { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.DirectoryNotFound); - } + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.DirectoryNotFound); } - // Folder exists, now verify whether the file itself exists. - attributes = GetAttributesNative(path); - - if (attributes == 0xFF) + // folder exists, lets verify whether the file itself exists + attributes = NativeIO.GetAttributes(path); + if (attributes == NativeIO.EmptyAttribute) { // No-op on file not found return; } - // Check if file is directory or read-only (then not allowed to delete) - if ((attributes & (byte)FileAttributes.Directory) != 0) + if ((attributes + & (uint)(FileAttributes.Directory | FileAttributes.ReadOnly)) != 0) { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); + // it's a readonly file or a directory + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); } - if ((attributes & (byte)FileAttributes.ReadOnly) != 0) - { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); - } - - DeleteNative(path); + NativeIO.Delete( + path, + false); } finally { - // TODO: File Handling missing. (Should not be possible to delete File in use!) + // regardless of what happened, we need to release the file when we're done + FileSystemManager.RemoveFromOpenList(record); } } @@ -142,10 +172,43 @@ public static void Delete(string path) /// Determines whether the specified file exists. /// /// The file to check. - /// true if the file exists; otherwise false. + /// if the caller has the required permissions and contains the name of an existing file; otherwise, . This method also returns if is , an invalid , or a zero-length string. If the caller does not have sufficient permissions to read the specified file, no exception is thrown and the method returns regardless of the existence of . public static bool Exists(string path) { - return ExistsNative(Path.GetDirectoryName(path), Path.GetFileName(path)); + try + { + // path validation happening in the call + path = Path.GetFullPath(path); + + // Is this the absolute root? this is not a file. + string root = Path.GetPathRoot(path); + + if (string.Equals(root, path)) + { + return false; + } + + uint attributes = NativeIO.GetAttributes(path); + + if (attributes == NativeIO.EmptyAttribute) + { + // this means not found + return false; + } + + if ((attributes + & (uint)FileAttributes.Directory) == 0) + { + // not a directory, it must be a file. + return true; + } + } + catch + { + // like the full .NET this does not throw exception in a number of cases, instead returns false. + } + + return false; } /// @@ -155,39 +218,25 @@ public static bool Exists(string path) /// cannot be not found. public static FileAttributes GetAttributes(string path) { - if (!Exists(path)) - { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.FileNotFound); - } + // path validation happening in the call + string fullPath = Path.GetFullPath(path); - var attributes = GetAttributesNative(path); + uint attributes = NativeIO.GetAttributes(fullPath); - if (attributes == 0xFF) + if (attributes == NativeIO.EmptyAttribute) { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.FileNotFound); + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.FileNotFound); } - - return (FileAttributes)attributes; - } - - /// - /// Returns the date and time the specified file or directory was last written to. - /// - /// - /// The file or directory for which to obtain write date and time information. - /// - /// - /// A structure set to the last write date and time for the specified file or directory. - /// - /// cannot be not found. - public static DateTime GetLastWriteTime(string path) - { - if (!Exists(path)) + else if (attributes == 0x0) { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.FileNotFound); + return FileAttributes.Normal; + } + else + { + return (FileAttributes)attributes; } - - return GetLastWriteTimeNative(path); } /// @@ -196,71 +245,153 @@ public static DateTime GetLastWriteTime(string path) /// The name of the file to move. Must be an absolute path. /// The new path and name for the file. /// or is or empty. - /// does not exist or exists. /// /// .NET nanoFramework implementation differs from the full framework as it requires that be an absolute path. This is a limitation coming from the platform. /// public static void Move(string sourceFileName, string destFileName) { - if (string.IsNullOrEmpty(sourceFileName)) - { - throw new ArgumentException(); - } + // sourceFileName and destFileName validation happening in the call + sourceFileName = Path.GetFullPath(sourceFileName); + destFileName = Path.GetFullPath(destFileName); - if (string.IsNullOrEmpty(destFileName)) - { - throw new ArgumentException(); - } + bool tryCopyAndDelete = false; - if (!Exists(sourceFileName)) - { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.FileNotFound); - } + // We only need to lock the source, not the dest because if dest is taken + // Move() will failed at the driver's level anyway. (there will be no conflict even if + // another thread is creating dest, as only one of the operations will succeed -- + // the native calls are atomic) + object srcRecord = FileSystemManager.AddToOpenList(sourceFileName); - if (Exists(destFileName)) + try { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.PathAlreadyExists); - } + if (!Exists(sourceFileName)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.FileNotFound); + } - if (sourceFileName == destFileName) - { - return; - } + if (Exists(destFileName)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.PathAlreadyExists); + } - // Check the volume of files - if (Path.GetPathRoot(sourceFileName) != Path.GetPathRoot(destFileName)) + // flag to try copy and delete ahead, in case this Move call returns false + tryCopyAndDelete = !NativeIO.Move( + sourceFileName, + destFileName); + } + finally { - // Cross Volume move (FAT_FS move not working) - Copy(sourceFileName, destFileName); - Delete(sourceFileName); + FileSystemManager.RemoveFromOpenList(srcRecord); } - else + + if (tryCopyAndDelete) { - // Same Volume (FAT_FS move) - MoveNative(sourceFileName, destFileName); + Copy( + sourceFileName, + destFileName, + false, + true); } } + /// + /// Opens a on the specified path with read/write access with no sharing. + /// + /// The file to open. + /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. + /// A opened in the specified mode and path, with read/write access and not shared. + public static FileStream Open( + string path, + FileMode mode) + { + return new FileStream( + path, + mode, + (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), + FileShare.None, + NativeFileStream.BufferSizeDefault); + } + + /// + /// Opens a FileStream on the specified path, with the specified mode and access with no sharing. + /// + /// The file to open. + /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. + /// A value that specifies the operations that can be performed on the file. + /// An unshared that provides access to the specified file, with the specified mode and access. + public static FileStream Open( + string path, + FileMode mode, + FileAccess access) + { + return new FileStream( + path, + mode, + access, + FileShare.None, + NativeFileStream.BufferSizeDefault); + } + + /// + /// Opens a FileStream on the specified path, having the specified mode with read, write, or read/write access and the specified sharing option. + /// + /// The file to open. + /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. + /// A value that specifies the operations that can be performed on the file. + /// A FileShare value specifying the type of access other threads have to the file. + /// A on the specified path, having the specified mode with read, write, or read/write access and the specified sharing option. + public static FileStream Open( + string path, + FileMode mode, + FileAccess access, + FileShare share) + { + return new FileStream( + path, + mode, + access, + share, + NativeFileStream.BufferSizeDefault); + } + /// /// Opens an existing file for reading. /// /// The file to be opened for reading. /// A on the specified path. - public static FileStream OpenRead(string path) => new(path, FileMode.Open, FileAccess.Read); + public static FileStream OpenRead(string path) => new FileStream( + path, + FileMode.Open, + FileAccess.Read, + NativeFileStream.BufferSizeDefault); /// /// Opens an existing UTF-8 encoded text file for reading. /// /// The file to be opened for reading. /// A on the specified path. - public static StreamReader OpenText(string path) => new(new FileStream(path, FileMode.Open, FileAccess.Read)); + public static StreamReader OpenText(string path) + { + // path validation happening in the call + path = Path.GetFullPath(path); + + return new StreamReader(OpenRead(path)); + } /// /// Opens an existing file or creates a new file for writing. /// /// The file to be opened for writing. - public static FileStream OpenWrite(string path) => new(path, FileMode.OpenOrCreate, FileAccess.Write); + public static FileStream OpenWrite(string path) => new FileStream( + path, + FileMode.OpenOrCreate, + FileAccess.Write, + NativeFileStream.BufferSizeDefault); /// /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. @@ -269,22 +400,36 @@ public static void Move(string sourceFileName, string destFileName) /// The end of the file was unexpectedly reached. public static byte[] ReadAllBytes(string path) { - using var stream = OpenRead(path); + byte[] bytes; + + using var fs = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + NativeFileStream.BufferSizeDefault); + + // blocking read + int index = 0; + long fileLength = fs.Length; + + if (fileLength > int.MaxValue) + { + throw new IOException(); + } - var index = 0; - var count = (int)stream.Length; - var bytes = new byte[count]; + int count = (int)fileLength; + bytes = new byte[count]; while (count > 0) { - var read = stream.Read(bytes, index, count > ChunkSize ? ChunkSize : count); - if (read <= 0) - { - throw new IOException(); - } + int n = fs.Read( + bytes, + index, + count); - index += read; - count -= read; + index += n; + count -= n; } return bytes; @@ -305,14 +450,19 @@ public static string ReadAllText(string path) /// /// The path to the file. /// A bitwise combination of the enumeration values. + /// cannot be not found." public static void SetAttributes(string path, FileAttributes fileAttributes) { if (!Exists(path)) { - throw new IOException(string.Empty, (int)IOException.IOExceptionErrorCode.FileNotFound); + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.FileNotFound); } - SetAttributesNative(path, (byte)fileAttributes); + NativeIO.SetAttributes( + path, + (byte)fileAttributes); } /// @@ -322,73 +472,98 @@ public static void SetAttributes(string path, FileAttributes fileAttributes) /// The bytes to write to the file. public static void WriteAllBytes(string path, byte[] bytes) { - if (string.IsNullOrEmpty(path)) + if (bytes == null) { - throw new ArgumentException(nameof(path)); + throw new ArgumentNullException(); } - if (bytes is null) - { - throw new ArgumentException(nameof(bytes)); - } + using var fs = new FileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + NativeFileStream.BufferSizeDefault); + + fs.Write( + bytes, + 0, + bytes.Length); + } + + /// + /// Creates a new file, writes the specified string to the file, and then closes the file. If the target file already exists, it is overwritten. + /// + /// The file to write to. + /// The string to write to the file. + public static void WriteAllText( + string path, + string contents) => WriteAllBytes( + path, + string.IsNullOrEmpty(contents) ? EmptyBytes : Encoding.UTF8.GetBytes(contents)); + + internal static void Copy( + string sourceFileName, + string destFileName, + bool overwrite, + bool deleteOriginal) + { + // sourceFileName and destFileName validation happening in the call - Create(path); + sourceFileName = Path.GetFullPath(sourceFileName); + destFileName = Path.GetFullPath(destFileName); - if (bytes.Length <= 0) - { - return; - } + FileMode writerMode = (overwrite) ? FileMode.Create : FileMode.CreateNew; + + var reader = new FileStream( + sourceFileName, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + NativeFileStream.BufferSizeDefault); - using var stream = new FileStream(path, FileMode.Open, FileAccess.Write); - for (var bytesWritten = 0L; bytesWritten < bytes.Length;) + try { - var bytesToWrite = bytes.Length - bytesWritten; - bytesToWrite = bytesToWrite < ChunkSize ? bytesToWrite : ChunkSize; + using var writer = new FileStream( + destFileName, + writerMode, + FileAccess.Write, + FileShare.None, + NativeFileStream.BufferSizeDefault); + + long fileLength = reader.Length; + + writer.SetLength(fileLength); - stream.Write(bytes, (int)bytesWritten, (int)bytesToWrite); - stream.Flush(); + byte[] buffer = new byte[ChunkSize]; - bytesWritten += bytesToWrite; + while (true) + { + int readSize = reader.Read(buffer, 0, ChunkSize); + + if (readSize <= 0) + { + break; + } + + writer.Write(buffer, 0, readSize); + } } - } + finally + { + // copy the attributes too + NativeIO.SetAttributes( + destFileName, + NativeIO.GetAttributes(sourceFileName)); - /// - /// Creates a new file, writes the specified string to the file, and then closes the file. If the target file already exists, it is overwritten. - /// - /// The file to write to. - /// The string to write to the file. - public static void WriteAllText(string path, string contents) => WriteAllBytes(path, string.IsNullOrEmpty(contents) ? EmptyBytes : Encoding.UTF8.GetBytes(contents)); - - #region Native Methods - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void DeleteNative(string path); - - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool ExistsNative(string path, string fileName); - - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern byte GetAttributesNative(string path); - - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern DateTime GetLastWriteTimeNative(string path); - - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void MoveNative(string pathSrc, string pathDest); - - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern void SetAttributesNative(string path, byte attributes); - #endregion + if (deleteOriginal) + { + reader.DisposeAndDelete(); + } + else + { + reader.Dispose(); + } + } + } } } diff --git a/System.IO.FileSystem/FileAttributes.cs b/System.IO.FileSystem/FileAttributes.cs index 1895e65..0008980 100644 --- a/System.IO.FileSystem/FileAttributes.cs +++ b/System.IO.FileSystem/FileAttributes.cs @@ -35,5 +35,10 @@ public enum FileAttributes /// This file is marked to be included in incremental backup operation. /// Archive = 0x20, + + /// + /// The file is a standard file that has no special attributes. This attribute is valid only if it is used alone. + /// + Normal = 0x80, } } diff --git a/System.IO.FileSystem/FileInfo.cs b/System.IO.FileSystem/FileInfo.cs new file mode 100644 index 0000000..2f0c3aa --- /dev/null +++ b/System.IO.FileSystem/FileInfo.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +namespace System.IO +{ + /// + /// Provides properties and instance methods for the creation, copying, deletion, moving, and opening of files, and aids in the creation of objects. This class cannot be inherited. + /// + [Serializable] + public sealed class FileInfo : FileSystemInfo + { + /// + /// Initializes a new instance of the class, which acts as a wrapper for a file path. + /// + /// + public FileInfo(string fileName) + { + // path validation in GetFullPath + _fullPath = Path.GetFullPath(fileName); + } + + /// + public override string Name + { + get + { + return Path.GetFileName(_fullPath); + } + } + + /// + /// Gets the size, in bytes, of the current file. + /// + public long Length + { + get + { + RefreshIfNull(); + + return _nativeFileInfo.Size; + } + } + + /// + /// Gets a string representing the directory's full path. + /// + public string DirectoryName + { + get + { + return Path.GetDirectoryName(_fullPath); + } + } + + /// + /// Gets an instance of the parent directory. + /// + public DirectoryInfo Directory + { + get + { + string dirName = DirectoryName; + + return dirName == null ? null : new DirectoryInfo(dirName); + } + } + + /// + /// Creates a file. + /// + /// A new file. + public FileStream Create() + { + return File.Create(_fullPath); + } + + /// + public override void Delete() + { + File.Delete(_fullPath); + } + + /// + public override bool Exists + { + get + { + return File.Exists(_fullPath); + } + } + + /// + protected override void HandleRefreshError() + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.FileNotFound); + } + + /// + /// Returns the original path that was passed to the FileInfo constructor. Use the or property for the full path or file name. + /// + /// A string representing the path. + public override string ToString() + { + return _fullPath; + } + } +} diff --git a/System.IO.FileSystem/FileStream.cs b/System.IO.FileSystem/FileStream.cs index 28fdc88..a73f24c 100644 --- a/System.IO.FileSystem/FileStream.cs +++ b/System.IO.FileSystem/FileStream.cs @@ -3,8 +3,6 @@ // See LICENSE file in the project root for full license information. // -using System.Runtime.CompilerServices; - namespace System.IO { /// @@ -12,19 +10,20 @@ namespace System.IO /// public class FileStream : Stream { - #region Variables + #region backing fields private bool _canRead; private bool _canWrite; private bool _canSeek; - private readonly long _seekLimit; - private long _position; + private long _seekLimit; private bool _disposed; - private readonly string _name; - private readonly string _path; + private readonly string _fileName; + + private NativeFileStream _nativeFileStream; + private FileSystemManager.FileRecord _fileRecord; #endregion @@ -33,56 +32,69 @@ public class FileStream : Stream /// /// Gets a value that indicates whether the current stream supports reading. /// - public override bool CanRead - { - get { return _canRead; } - } + /// if the stream supports reading; otherwise, . + public override bool CanRead => _canRead; /// /// Gets a value that indicates whether the current stream supports seeking. /// - public override bool CanSeek - { - get { return _canSeek; } - } + /// if the stream supports seeking; if the stream is closed or if the was constructed from an operating-system handle such as a pipe or output to the console. + public override bool CanSeek => _canSeek; /// /// Gets a value that indicates whether the current stream supports writing. /// - public override bool CanWrite - { - get { return _canWrite; } - } + /// if the stream supports writing; if the stream is closed or was opened with read-only access. + public override bool CanWrite => _canWrite; /// /// Gets the length in bytes of the stream. /// + /// The length in bytes of the stream. public override long Length { get - { + { if (_disposed) { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters throw new ObjectDisposedException(); +#pragma warning restore S2372 // Exceptions should not be thrown from property getters } - return GetLengthNative(FilePath, Name); + if (!_canSeek) + { + throw new NotSupportedException(); + } + + return _nativeFileStream.GetLength(); } } /// /// Gets or sets the current position of this stream. /// + /// The current position of this stream. public override long Position { get { if (_disposed) { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters throw new ObjectDisposedException(); +#pragma warning restore S2372 // Exceptions should not be thrown from property getters + } + + if (!_canSeek) + { + throw new NotSupportedException(); } - return !_canSeek ? throw new NotSupportedException() : _position; + // argument validation in interop layer + return _nativeFileStream.Seek( + 0, + (uint)SeekOrigin.Current); } set @@ -99,37 +111,30 @@ public override long Position if (value < _seekLimit) { - throw new ArgumentException("Can't set Position below SeekLimit."); + throw new IOException(); } - if (value > Length) - { - throw new ArgumentException("Can't set Position beyond end of File."); - } - - _position = value; + // argument validation in interop layer + _ = _nativeFileStream.Seek( + value, + (uint)SeekOrigin.Begin); } } /// - /// Gets the name of the file including the file name extension. + /// Gets the absolute path of the file opened in the . /// /// - /// The name of the file including the file name extension. + /// A string that is the absolute path of the file. /// - public string Name => _name; - - /// - /// Gets the full file-system path of the current file, if the file has a path. - /// - public string FilePath => _path; + public string Name => _fileName; #endregion - #region Constructor + #region Constructors /// - /// Initializes a new instance of the FileStream class with the specified path and creation mode. + /// Initializes a new instance of the class with the specified path and creation mode. /// /// A relative or absolute path for the file that the current FileStream object will encapsulate. /// One of the enumeration values that determines how to open or create the file. @@ -139,21 +144,81 @@ public FileStream( : this( path, mode, - (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite)) + (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), + FileShare.Read, + NativeFileStream.BufferSizeDefault) { } /// - /// Initializes a new instance of the FileStream class with the specified path, creation mode, and read/write permission. + /// Initializes a new instance of the class with the specified path, creation mode, and read/write permission. /// /// A relative or absolute path for the file that the current FileStream object will encapsulate. /// One of the enumeration values that determines how to open or create the file. - /// A bitwise combination of the enumeration values that determines how the file can be accessed by the FileStream object. This also determines the values returned by the CanRead and CanWrite properties of the FileStream object. + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. is if path specifies a disk file. public FileStream( string path, FileMode mode, FileAccess access) + : this( + path, + mode, + access, + FileShare.Read, + NativeFileStream.BufferSizeDefault) { + } + + /// + /// Initializes a new instance of the FileStream class with the specified path, creation mode, read/write permission, and sharing permission. + /// + /// A relative or absolute path for the file that the current FileStream object will encapsulate. + /// One of the enumeration values that determines how to open or create the file. + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. is if path specifies a disk file. + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. + public FileStream( + string path, + FileMode mode, + FileAccess access, + FileShare share) + : this( + path, + mode, + access, + share, + NativeFileStream.BufferSizeDefault) + { + } + + /// + /// Initializes a new instance of the FileStream class with the specified path, creation mode, read/write permission, and sharing permission. + /// + /// A relative or absolute path for the file that the current FileStream object will encapsulate. + /// One of the enumeration values that determines how to open or create the file. + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. is if path specifies a disk file. + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. + /// A positive value greater than 0 indicating the buffer size. The default buffer size is 2048. + public FileStream( + string path, + FileMode mode, + FileAccess access, + FileShare share, + int bufferSize) + { + // path validation happening in the call + _fileName = Path.GetFullPath(path); + + // make sure mode, access, and share are within range + if (mode < FileMode.CreateNew + || mode > FileMode.Append + || access < FileAccess.Read + || access > FileAccess.ReadWrite + || share < FileShare.None + || share > FileShare.ReadWrite) + { + throw new ArgumentOutOfRangeException(); + } + // Get wantsRead and wantsWrite from access, note that they cannot both be false bool wantsRead = (access & FileAccess.Read) == FileAccess.Read; bool wantsWrite = (access & FileAccess.Write) == FileAccess.Write; @@ -164,141 +229,79 @@ public FileStream( && mode != FileMode.OpenOrCreate && !wantsWrite) { -#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one throw new ArgumentException(); -#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one } - // Set File Name & Path - _name = Path.GetFileName(path); - _path = Path.GetDirectoryName(path); + RegisterShareInformation(access, share); - // TODO: Get Readonly status from File? Necessary? - bool exists = File.Exists(path); - bool isReadOnly = exists && ((File.GetAttributes(path)) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; + try + { + uint attributes = NativeIO.GetAttributes(_fileName); + bool exists = attributes != NativeIO.EmptyAttribute; + bool isReadOnly = exists && (((FileAttributes)attributes) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; - // The seek limit is 0 (the beginning of the file) for all modes except Append - _seekLimit = 0; + // if the path specified is an existing directory, fail + if (exists && ((((FileAttributes)attributes) + & FileAttributes.Directory) == FileAttributes.Directory)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); + } - // Set actual position to Start of File - _position = 0; + // The seek limit is 0 (the beginning of the file) for all modes except Append + _seekLimit = 0; - switch (mode) - { - case FileMode.CreateNew: - // if the file exists, IOException is thrown - if (exists) - { - throw new IOException("File already exists.", (int)IOException.IOExceptionErrorCode.PathAlreadyExists); - } + switch (mode) + { + case FileMode.CreateNew: + CreateNewFile(exists, bufferSize); + break; - OpenFileNative(FilePath, Name, (int) mode); - break; + case FileMode.Create: + CreateFile(exists, bufferSize); + break; - case FileMode.Create: - // if the file exists, it should be overwritten - OpenFileNative(FilePath, Name, (int)mode); - - break; + case FileMode.Open: + OpenFile(exists, bufferSize); + break; - case FileMode.Open: - // if the file does not exist, IOException/FileNotFound is thrown - if (!exists) - { - throw new IOException("File not found/exists.", (int)IOException.IOExceptionErrorCode.FileNotFound); - } - - OpenFileNative(FilePath, Name, (int)mode); - - break; - - case FileMode.OpenOrCreate: - // if the file does not exist, it is created - OpenFileNative(FilePath, Name, (int)mode); - - break; - - case FileMode.Truncate: - // the file would be overwritten. if the file does not exist, IOException/FileNotFound is thrown - if (!exists) - { - throw new IOException("File not found/existing.", (int)IOException.IOExceptionErrorCode.FileNotFound); - } + case FileMode.OpenOrCreate: + OpenOrCreateFile(bufferSize); + break; - OpenFileNative(FilePath, Name, (int)mode); - - break; + case FileMode.Truncate: + TruncateFile(exists, bufferSize); + break; - case FileMode.Append: - // Opens the file if it exists and seeks to the end of the file. Append can only be used in conjunction with FileAccess.Write - // Attempting to seek to a position before the end of the file will throw an IOException and any attempt to read fails and throws an NotSupportedException - if (access != FileAccess.Write) - { - throw new ArgumentException("No Write Access to file."); - } + case FileMode.Append: + AppendToFile(access, bufferSize); + break; - OpenFileNative(FilePath, Name, (int)mode); - - _position = Length; - _seekLimit = Length; - - break; + default: + throw new ArgumentException(""); + } - default: - throw new ArgumentException("FileMode not known."); - } + // Now that we have a valid NativeFileStream, we add it to the FileRecord, so it can get cleaned-up in case an eject or force format + _fileRecord.NativeFileStream = _nativeFileStream; - switch(access) - { - case FileAccess.Read: - _canRead = true; - _canSeek = true; - break; - - case FileAccess.Write: - _canWrite = true; - _canSeek = true; - break; - - case FileAccess.ReadWrite: - _canRead = true; - _canWrite = true; - _canSeek = true; - break; - - default: -#pragma warning disable S112 // General exceptions should never be thrown - throw new Exception("FileAccess not known."); -#pragma warning restore S112 // General exceptions should never be thrown + AdjustCapabilities( + wantsRead, + wantsWrite, + isReadOnly); } - - // If the file is ReadOnly, regardless of the file system capability, we'll turn off write - if (isReadOnly) + catch { - _canWrite = false; - } + // something went wrong, clean up and re-throw the exception + _nativeFileStream?.Close(); - // Make sure the requests (wantsRead / wantsWrite) matches the file system capabilities (canRead / canWrite) - if ((wantsRead && !_canRead) - || (wantsWrite && !_canWrite)) - { - throw new IOException("", (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); - } + FileSystemManager.RemoveFromOpenList(_fileRecord); - // Finally, adjust the _canRead / _canWrite to match the requests - if (!wantsWrite) - { - _canWrite = false; - } - else if (!wantsRead) - { - _canRead = false; + throw; } } - - /// - /// Destructor - /// + + /// ~FileStream() { Dispose(false); @@ -321,7 +324,7 @@ public override void Close() /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) - { + { if (!_disposed) { try @@ -332,26 +335,57 @@ protected override void Dispose(bool disposing) _canWrite = false; _canSeek = false; } + + _nativeFileStream?.Close(); + + base.Dispose(disposing); } finally { + if (_fileRecord != null) + { + FileSystemManager.RemoveFromOpenList(_fileRecord); + _fileRecord = null; + } + + _nativeFileStream = null; _disposed = true; } } } + // This is for internal use to support proper atomic CopyAndDelete + internal void DisposeAndDelete() + { + _nativeFileStream.Close(); + + // need to null this so Dispose(true) won't close the stream again + _nativeFileStream = null; + + NativeIO.Delete( + _fileName, + false); + + Dispose(true); + } + /// /// Clears buffers for this stream and causes any buffered data to be written to the file. /// public override void Flush() { - // Already everything flushed/sync after every Read/Write operation, nothing to do here. + if (_disposed) + { + throw new ObjectDisposedException(); + } + + _nativeFileStream.Flush(); } /// /// Reads a block of bytes from the stream and writes the data in a given buffer. /// - /// When this method returns, contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// When this method returns, contains the specified byte array with the values between and ( + - 1) replaced by the bytes read from the current source. /// The byte offset in array at which the read bytes will be placed. /// The maximum number of bytes to read. /// The total number of bytes read into the buffer. This might be less than the number of bytes requested if that number of bytes are not currently available, or zero if the end of the stream is reached. @@ -370,44 +404,32 @@ public override int Read( throw new NotSupportedException(); } - //Checks - if (offset > buffer.Length) + lock (_nativeFileStream) { - throw new IndexOutOfRangeException("Offset is outside of buffer size."); + // argument validation in interop layer + return _nativeFileStream.Read( + buffer, + offset, + count, + NativeFileStream.TimeoutDefault); } + } - if (buffer.Length < offset + count) - { - throw new IndexOutOfRangeException("Buffer size is smaller then offset + byteCount."); - } - - // Create buffer for read Data - byte[] readedBuffer = new byte[count]; - - int readedCount = ReadNative( - FilePath, - Name, - Position, - readedBuffer, - count); - - // Copy Data into source Buffer - Array.Copy( - readedBuffer, - 0, - buffer, - offset, - count); - - _position += readedCount; // Adapt new actual position - - return readedCount; + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// The buffer to write the data into. + /// The total number of bytes read into the buffer. This might be less than the number of bytes requested if that number of bytes are not currently available, or zero if the end of the stream is reached. + /// This method is currently not implemented. + public override int Read(SpanByte buffer) + { + throw new NotImplementedException(); } /// /// Reads a byte from the file and advances the read position one byte. /// - /// + /// The byte, cast to an , or -1 if the end of the stream has been reached. public override int ReadByte() { byte[] resByte = new byte[1]; @@ -415,7 +437,10 @@ public override int ReadByte() int count = Read(resByte, 0, 1); if (count != 1) - return -1; // End of File + { + // End of File + return -1; + } return resByte[0]; } @@ -424,12 +449,12 @@ public override int ReadByte() /// Sets the current position of this stream to the given value. /// /// The point relative to origin from which to begin seeking. - /// Specifies the beginning, the end, or the current position as a reference point for offset, using a value of type SeekOrigin. + /// Specifies the beginning, the end, or the current position as a reference point for , using a value of type . /// The new position in the stream. public override long Seek( long offset, SeekOrigin origin) - { + { if (_disposed) { throw new ObjectDisposedException(); @@ -440,29 +465,19 @@ public override long Seek( throw new NotSupportedException(); } - long newPosition; + long oldPosition = Position; - switch(origin) - { - case SeekOrigin.Begin: - newPosition = 0 + offset; - break; - - case SeekOrigin.Current: - newPosition = Position + offset; - break; + long newPosition = _nativeFileStream.Seek( + offset, + (uint)origin); - case SeekOrigin.End: - newPosition = Length + offset; - break; + if (newPosition < _seekLimit) + { + Position = oldPosition; - default: - throw new NotSupportedException("SeekOrigin (" + origin.ToString() + ") not supported."); + throw new IOException(); } - // Try to set new Position - Position = newPosition; - return newPosition; } @@ -472,7 +487,19 @@ public override long Seek( /// The new length of the stream. public override void SetLength(long value) { - throw new NotImplementedException(); + if (_disposed) + { + throw new ObjectDisposedException(); + } + + if (!_canWrite + || !_canSeek) + { + throw new NotSupportedException(); + } + + // argument validation in interop layer + _nativeFileStream.SetLength(value); } /// @@ -493,34 +520,29 @@ public override void Write(byte[] buffer, int offset, int count) throw new NotSupportedException(); } - //Checks - if (offset > buffer.Length) - { - throw new IndexOutOfRangeException("Offset is outside of buffer size."); - } + // argument validation in interop layer + int bytesWritten; - if (buffer.Length < offset + count) + lock (_nativeFileStream) { - throw new IndexOutOfRangeException("Buffer size is smaller then offset + byteCount."); - } - - byte[] bufferToWrite = new byte[count]; - - Array.Copy( - buffer, - offset, - bufferToWrite, - 0, - count); + // we check for count being != 0 because we want to handle negative cases as well in the interop layer + while (count != 0) + { + bytesWritten = _nativeFileStream.Write( + buffer, + offset, + count, + NativeFileStream.TimeoutDefault); - WriteNative( - FilePath, - Name, - Position, - bufferToWrite, - count); + if (bytesWritten == 0) + { + throw new IOException(); + } - _position += count; // Adapt new actual position + offset += bytesWritten; + count -= bytesWritten; + } + } } /// @@ -536,34 +558,145 @@ public override void WriteByte(byte value) #endregion - #region Stubs (Native Calls) + private void AdjustCapabilities(bool wantsRead, bool wantsWrite, bool isReadOnly) + { + // Retrive the filesystem capabilities + _nativeFileStream.GetStreamProperties( + out _canRead, + out _canWrite, + out _canSeek); + + // Ii the file is readonly, regardless of the filesystem capability, we'll turn off write + if (isReadOnly) + { + _canWrite = false; + } + + // Make sure the requests (wantsRead / wantsWrite) matches the filesystem capabilities (canRead / canWrite) + if ((wantsRead + && !_canRead) || (wantsWrite + && !_canWrite)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private extern void OpenFileNative(string path, string fileName, int fileMode); + // finally, adjust the _canRead / _canWrite to match the requests + if (!wantsWrite) + { + _canWrite = false; + } + else if (!wantsRead) + { + _canRead = false; + } + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private extern int ReadNative(string path, string fileName, long actualPosition, byte[] buffer, int length); + private void RegisterShareInformation( + FileAccess access, + FileShare share) + { + _fileRecord = FileSystemManager.AddToOpenList( + _fileName, + access, + share); + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private extern void WriteNative(string path, string fileName, long actualPosition, byte[] buffer, int length); + private void CreateNewFile( + bool exists, + int bufferSize) + { + // if the file exists, IOException is thrown + if (exists) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.PathAlreadyExists); + } - [Diagnostics.DebuggerStepThrough] - [Diagnostics.DebuggerHidden] - [MethodImpl(MethodImplOptions.InternalCall)] - private extern long GetLengthNative(string path, string fileName); + _nativeFileStream = new NativeFileStream( + _fileName, + bufferSize); + } - public override int Read(SpanByte buffer) + private void CreateFile( + bool exists, + int bufferSize) { - throw new NotImplementedException(); + // if the file exists, it should be overwritten + _nativeFileStream = new NativeFileStream( + _fileName, + bufferSize); + + if (exists) + { + _nativeFileStream.SetLength(0); + } } - #endregion + private void OpenFile( + bool exists, + int bufferSize) + { + // if the file does not exist, IOException/FileNotFound is thrown + if (!exists) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.FileNotFound); + } + + _nativeFileStream = new NativeFileStream( + _fileName, + bufferSize); + } + + private void OpenOrCreateFile(int bufferSize) + { + // if the file does not exist, it is created + _nativeFileStream = new NativeFileStream( + _fileName, + bufferSize); + } + + private void TruncateFile( + bool exists, + int bufferSize) + { + // the file would be overwritten. if the file does not exist, IOException/FileNotFound is thrown + if (!exists) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.FileNotFound); + } + + _nativeFileStream = new NativeFileStream( + _fileName, + bufferSize); + + _nativeFileStream.SetLength(0); + } + + private void AppendToFile( + FileAccess access, + int bufferSize) + { + // Opens the file if it exists and seeks to the end of the file. Append can only be used in conjunction with FileAccess.Write + // Attempting to seek to a position before the end of the file will throw an IOException and any attempt to read fails and throws an NotSupportedException + if (access != FileAccess.Write) + { + throw new ArgumentException(""); + } + _nativeFileStream = new NativeFileStream( + _fileName, + bufferSize); + + _seekLimit = _nativeFileStream.Seek( + 0, + (uint)SeekOrigin.End); + } } } diff --git a/System.IO.FileSystem/FileSystemInfo.cs b/System.IO.FileSystem/FileSystemInfo.cs new file mode 100644 index 0000000..5e073d9 --- /dev/null +++ b/System.IO.FileSystem/FileSystemInfo.cs @@ -0,0 +1,115 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace System.IO +{ + /// + /// Provides the base class for both and objects. + /// + public abstract class FileSystemInfo : MarshalByRefObject + { + internal NativeFileInfo _nativeFileInfo; + + // fully qualified path of the directory + internal string _fullPath; + + /// + /// Gets or sets the attributes for the current file or directory. + /// + public FileAttributes Attributes + { + get + { + RefreshIfNull(); + + return (FileAttributes)_nativeFileInfo.Attributes; + } + } + + /// + /// Gets the full path of the directory or file. + /// + public virtual string FullName + { + get + { + return _fullPath; + } + } + + /// + /// Gets the extension part of the file name, including the leading dot . even if it is the entire file name, or an empty string if no extension is present. + /// + /// A string containing the FileSystemInfo extension. + public string Extension + { + get + { + return Path.GetExtension(FullName); + } + } + + /// + /// Gets a value indicating whether the file or directory exists. + /// + public abstract bool Exists + { + get; + } + + /// + /// Gets the name of the file. + /// + public abstract string Name + { + get; + } + + /// + /// Deletes a file or directory. + /// + public abstract void Delete(); + + /// + /// Refreshes the state of the object. + /// + /// A device such as a disk drive is not ready. + public virtual void Refresh() + { + object record = FileSystemManager.AddToOpenListForRead(_fullPath); + + try + { + _nativeFileInfo = NativeFindFile.GetFileInfo(_fullPath); + + if (_nativeFileInfo == null) + { + HandleRefreshError(); + } + } + finally + { + FileSystemManager.RemoveFromOpenList(record); + } + } + + /// + /// Handler for the case when the file or directory does not exist. + /// + protected abstract void HandleRefreshError(); + + internal void RefreshIfNull() + { + if (_nativeFileInfo == null) + { + Refresh(); + } + } + } +} diff --git a/System.IO.FileSystem/FileSystemManager.cs b/System.IO.FileSystem/FileSystemManager.cs new file mode 100644 index 0000000..7c22529 --- /dev/null +++ b/System.IO.FileSystem/FileSystemManager.cs @@ -0,0 +1,236 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System.Collections; + +namespace System.IO +{ + internal class FileSystemManager + { + private static readonly ArrayList _openFiles = new(); + private static readonly ArrayList _lockedDirs = new(); + private static object _currentDirectoryRecord = null; + + internal static string CurrentDirectory = NativeIO.FSRoot; + + internal class FileRecord + { + public string FullName; + public NativeFileStream NativeFileStream; + public FileShare Share; + + public FileRecord( + string fullName, + FileShare share) + { + FullName = fullName; + Share = share; + } + } + + public static object AddToOpenListForRead(string fullName) + { + return AddToOpenList( + fullName, + FileAccess.Read, + FileShare.ReadWrite); + } + + public static object AddToOpenList(string fullName) + { + return AddToOpenList( + fullName, + FileAccess.ReadWrite, + FileShare.None); + } + + public static FileRecord AddToOpenList( + string fullName, + FileAccess access, + FileShare share) + { + fullName = fullName.ToUpper(); + + FileRecord record = new FileRecord( + fullName, + share); + + lock (_openFiles) + { + int count = _lockedDirs.Count; + + for (int i = 0; i < count; i++) + { + if (IsInDirectory(fullName, (string)_lockedDirs[i])) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); + } + } + + FileRecord current; + count = _openFiles.Count; + + for (int i = 0; i < count; ++i) + { + current = (FileRecord)_openFiles[i]; + + if (current.FullName == fullName) + { + // Given the previous fileshare info and the requested fileaccess and fileshare + // the following is the ONLY combinations that we should allow -- All others + // should failed with IOException + // (Behavior verified on desktop .NET) + // + // Previous FileShare Requested FileAccess Requested FileShare + // Read Read ReadWrite + // Write Write ReadWrite + // ReadWrite Read ReadWrite + // ReadWrite Write ReadWrite + // ReadWrite ReadWrite ReadWrite + // + // The following check take advantage of the fact that the value for + // Read, Write, and ReadWrite in FileAccess enum and FileShare enum are + // identical. + + if ((share != FileShare.ReadWrite) + || (((int)current.Share & (int)access) != (int)access)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); + } + } + } + + _openFiles.Add(record); + } + + return record; + } + + public static void RemoveFromOpenList(object record) + { + lock (_openFiles) + { + _openFiles.Remove(record); + } + } + + public static object LockDirectory(string directory) + { + directory = directory.ToUpper(); + + lock (_openFiles) + { + if (_lockedDirs.Contains(directory) || AnyFileOpenInDirectory(directory)) + { + throw new IOException( + string.Empty, + (int)IOException.IOExceptionErrorCode.UnauthorizedAccess); + } + + _lockedDirs.Add(directory); + } + + return (object)directory; + } + + private static bool AnyFileOpenInDirectory(string directory) + { + foreach (FileRecord record in _openFiles) + { + if (IsInDirectory(record.FullName, directory)) + { + return true; + } + } + + return false; + } + + public static void UnlockDirectory(object record) + { + lock (_openFiles) + { + _lockedDirs.Remove(record); + } + } + + public static void UnlockDirectory(string directory) + { + directory = directory.ToUpper(); + + lock (_openFiles) + { + _lockedDirs.Remove(directory); + } + } + + public static void ForceRemoveRootname(string rootNamme) + { + string root = rootNamme.ToUpper(); + ArrayList recordsToRemove = new(); + + lock (_openFiles) + { + foreach (FileRecord record in _openFiles) + { + if (IsInDirectory(record.FullName, root)) + { + record.NativeFileStream?.Close(); + recordsToRemove.Add(record); + } + } + + foreach (FileRecord record in recordsToRemove) + { + _openFiles.Remove(record); + } + } + } + + public static bool IsInDirectory( + string path, + string directory) + { + if (path.IndexOf(directory) == 0) + { + int directoryLength = directory.Length; + + if (path.Length > directoryLength) + { + return path[directoryLength] == '\\'; + } + else + { + return true; + } + } + + return false; + } + + internal static void SetCurrentDirectory(string path) + { + if (_currentDirectoryRecord != null) + { + RemoveFromOpenList(_currentDirectoryRecord); + } + + if (path != NativeIO.FSRoot) + { + _currentDirectoryRecord = AddToOpenListForRead(path); + } + else + { + _currentDirectoryRecord = null; + } + + CurrentDirectory = path; + } + } +} diff --git a/System.IO.FileSystem/NativeFileInfo.cs b/System.IO.FileSystem/NativeFileInfo.cs new file mode 100644 index 0000000..f7d72bd --- /dev/null +++ b/System.IO.FileSystem/NativeFileInfo.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +namespace System.IO +{ + internal class NativeFileInfo + { + public uint Attributes; + public long Size; + public string FileName; + } +} diff --git a/System.IO.FileSystem/NativeFileStream.cs b/System.IO.FileSystem/NativeFileStream.cs new file mode 100644 index 0000000..77374af --- /dev/null +++ b/System.IO.FileSystem/NativeFileStream.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System.Runtime.CompilerServices; + +namespace System.IO +{ + internal class NativeFileStream + { + // field is required for native interop +#pragma warning disable IDE0051 +#pragma warning disable CS0169 +#pragma warning disable S1144 + object _fs; +#pragma warning restore S1144 +#pragma warning restore CS0169 +#pragma warning restore IDE0051 + + public const int TimeoutDefault = 0; + public const int BufferSizeDefault = 0; + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern NativeFileStream( + string path, + int bufferSize); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern int Read( + byte[] buf, + int offset, + int count, + int timeout); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern int Write( + byte[] buf, + int offset, + int count, + int timeout); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern long Seek( + long offset, + uint origin); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern void Flush(); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern long GetLength(); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern void SetLength(long length); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern void GetStreamProperties( + out bool canRead, + out bool canWrite, + out bool canSeek); + + [MethodImpl(MethodImplOptions.InternalCall)] + public extern void Close(); + } +} diff --git a/System.IO.FileSystem/NativeFindFile.cs b/System.IO.FileSystem/NativeFindFile.cs new file mode 100644 index 0000000..fa96537 --- /dev/null +++ b/System.IO.FileSystem/NativeFindFile.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.IO +{ + internal class NativeFindFile + { + [MethodImpl(MethodImplOptions.InternalCall)] + [DebuggerNonUserCode] + public static extern NativeFileInfo GetFileInfo(string path); + } +} diff --git a/System.IO.FileSystem/NativeIO.cs b/System.IO.FileSystem/NativeIO.cs new file mode 100644 index 0000000..b0e4bc7 --- /dev/null +++ b/System.IO.FileSystem/NativeIO.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.IO +{ + internal static class NativeIO + { + internal const string FSRoot = @"\"; + + //////////////////////////////////////////////////////////////////////////////////////////////// + // !!! KEEP IN SYNC WITH src\System.IO.FileSystem\nf_sys_io_filesystem.h (in native code) !!! // + //////////////////////////////////////////////////////////////////////////////////////////////// + internal const uint EmptyAttribute = 0xFFFFFFFF; + + ///////////////////////////////////////////////////////////////////////////////////// + // !!! KEEP IN SYNC WITH src\PAL\Include\nanoPAL_FileSystem.h (in native code) !!! // + ///////////////////////////////////////////////////////////////////////////////////// + internal const int FSMaxPathLength = 260 - 2; + internal const int FSMaxFilenameLength = 256; + internal const int FSNameMaxLength = 7 + 1; + + [DebuggerNonUserCode] + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Delete(string path, bool recursive); + + [DebuggerNonUserCode] + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Move( + string sourceFileName, + string destFileName); + + [DebuggerNonUserCode] + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void CreateDirectory(string path); + + [DebuggerNonUserCode] + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern uint GetAttributes(string path); + + [DebuggerNonUserCode] + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void SetAttributes( + string path, + uint attributes); + + [DebuggerNonUserCode] + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Format( + string rootName, + string fileSystem, + uint parameter); + } +} diff --git a/System.IO.FileSystem/Path.cs b/System.IO.FileSystem/Path.cs index ca6060c..1fcfcab 100644 --- a/System.IO.FileSystem/Path.cs +++ b/System.IO.FileSystem/Path.cs @@ -283,9 +283,8 @@ public static string GetFullPath(string path) if (!IsPathRooted(path)) { - // TODO: will be implemented in next PR - // string currDir = Directory.GetCurrentDirectory(); - // path = Combine(currDir, path); + string currDir = Directory.GetCurrentDirectory(); + path = Combine(currDir, path); } return PathInternal.NormalizeDirectorySeparators(path); diff --git a/System.IO.FileSystem/PathInternal.cs b/System.IO.FileSystem/PathInternal.cs index 92df7ed..818770c 100644 --- a/System.IO.FileSystem/PathInternal.cs +++ b/System.IO.FileSystem/PathInternal.cs @@ -241,56 +241,75 @@ internal static string NormalizeDirectorySeparators(string path) return path; } - char current; + bool hasNavigationComponents = false; - // Make a pass to see if we need to normalize so we can potentially skip allocating - var normalized = true; + var components = path.Split(new[] + { + DirectorySeparatorChar, + AltDirectorySeparatorChar + }); + var resultComponents = new string[components.Length]; + var resultIndex = 0; - for (var i = 0; i < path.Length; i++) + for (int i = 0; i < components.Length; i++) { - current = path[i]; - if (IsDirectorySeparator(current) - && (current != DirectorySeparatorChar - // Check for sequential separators past the first position (we need to keep initial two for UNC/extended) - || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1])))) + var component = components[i]; + + if (component == "..") + { + // We're navigating, so remember that + hasNavigationComponents = true; + + // If the previous component is also "..", or if it's the start of the path, add ".." to the result + if (i == 0 && (resultIndex == 0 + || resultComponents[resultIndex - 1] == "..")) + { + resultComponents[resultIndex] = ".."; + resultIndex++; + } + else + { + // Otherwise, go up one directory level, if we're not already at the root + if (resultIndex > 0) + { + resultIndex--; + } + } + } + else if (component != "." + && component != "") { - normalized = false; - break; + resultComponents[resultIndex] = component; + resultIndex++; } } - if (normalized) - { - return path; - } + var builder = new StringBuilder(); - var builder = new StringBuilder(MaxShortPath); - var start = 0; - - if (IsDirectorySeparator(path[start])) + // if the original path started with a directory separator, ensure the result does too + // unless there were navigation components, in which case we can't start with a separator + if (!hasNavigationComponents && (path.StartsWith(DirectorySeparatorCharAsString) + || path.StartsWith(AltDirectorySeparatorChar.ToString()))) { - start++; builder.Append(DirectorySeparatorChar); } - for (var i = start; i < path.Length; i++) + for (var i = 0; i < resultIndex; i++) { - current = path[i]; - - // If we have a separator - if (IsDirectorySeparator(current)) + if (i > 0 + && builder.Length > 0) { - // If the next is a separator, skip adding this - if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1])) - { - continue; - } - - // Ensure it is the primary separator - current = DirectorySeparatorChar; + builder.Append(DirectorySeparatorChar); } - builder.Append(current); + builder.Append(resultComponents[i]); + } + + // if the original path ended with a directory separator, ensure the result does too + if (path.EndsWith(DirectorySeparatorCharAsString) + || path.EndsWith(AltDirectorySeparatorChar.ToString())) + { + builder.Append(DirectorySeparatorChar); } return builder.ToString(); diff --git a/System.IO.FileSystem/Properties/AssemblyInfo.cs b/System.IO.FileSystem/Properties/AssemblyInfo.cs index c4c7ac2..a63aad4 100644 --- a/System.IO.FileSystem/Properties/AssemblyInfo.cs +++ b/System.IO.FileSystem/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////// // update this whenever the native assembly signature changes // -[assembly: AssemblyNativeVersion("1.1.0.0")] +[assembly: AssemblyNativeVersion("1.1.0.3")] //////////////////////////////////////////////////////////////// [assembly: InternalsVisibleTo("NFUnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001001120aa3e809b3da4f65e1b1f65c0a3a1bf6335c39860ca41acb3c48de278c6b63c5df38239ec1f2e32d58cb897c8c174a5f8e78a9c0b6087d3aef373d7d0f3d9be67700fc2a5a38de1fb71b5b6f6046d841ff35abee2e0b0840a6291a312be184eb311baff5fef0ff6895b9a5f2253aed32fb06b819134f6bb9d531488a87ea2")] diff --git a/System.IO.FileSystem/System.IO.FileSystem.nfproj b/System.IO.FileSystem/System.IO.FileSystem.nfproj index 269186d..f56764a 100644 --- a/System.IO.FileSystem/System.IO.FileSystem.nfproj +++ b/System.IO.FileSystem/System.IO.FileSystem.nfproj @@ -42,18 +42,26 @@ + + + + + - + + + + @@ -72,23 +80,18 @@ ..\packages\nanoFramework.CoreLibrary.1.15.5\lib\mscorlib.dll - True ..\packages\nanoFramework.Runtime.Events.1.11.18\lib\nanoFramework.Runtime.Events.dll - True ..\packages\nanoFramework.System.Runtime.1.0.27\lib\nanoFramework.System.Runtime.dll - True ..\packages\nanoFramework.System.Text.1.2.54\lib\nanoFramework.System.Text.dll - True ..\packages\nanoFramework.System.IO.Streams.1.1.59\lib\System.IO.Streams.dll - True diff --git a/System.IO.FileSystem/nanoFramework/RemovableDeviceEventArgs.cs b/System.IO.FileSystem/nanoFramework/RemovableDriveEventArgs.cs similarity index 62% rename from System.IO.FileSystem/nanoFramework/RemovableDeviceEventArgs.cs rename to System.IO.FileSystem/nanoFramework/RemovableDriveEventArgs.cs index 9cd4611..f818839 100644 --- a/System.IO.FileSystem/nanoFramework/RemovableDeviceEventArgs.cs +++ b/System.IO.FileSystem/nanoFramework/RemovableDriveEventArgs.cs @@ -4,44 +4,35 @@ // using System; +using System.IO; -namespace nanoFramework.System.IO.FileSystem +namespace nanoFramework.System.IO { /// - /// Contains argument values for Removable Devices events. + /// Contains argument values for Removable Drive events. /// - public class RemovableDeviceEventArgs : EventArgs + public class RemovableDriveEventArgs : EventArgs { - private readonly string _path; + private readonly DriveInfo _drive; private readonly RemovableDeviceEvent _event; - internal RemovableDeviceEventArgs(string path, RemovableDeviceEvent deviceEvent) + internal RemovableDriveEventArgs( + DriveInfo drive, + RemovableDeviceEvent deviceEvent) { - _path = path; + _drive = drive; _event = deviceEvent; } /// - /// The path of the Removable Device. + /// The of the removable drive. /// - public string Path - { - get - { - return _path; - } - } + public DriveInfo Drive => _drive; /// /// The occurred. /// - public RemovableDeviceEvent Event - { - get - { - return _event; - } - } + public RemovableDeviceEvent Event => _event; /// /// Specifies the type of event occurred with the Removable Device specified. diff --git a/System.IO.FileSystem/nanoFramework/StorageEventManager.cs b/System.IO.FileSystem/nanoFramework/StorageEventManager.cs index f62b0a5..5f818b3 100644 --- a/System.IO.FileSystem/nanoFramework/StorageEventManager.cs +++ b/System.IO.FileSystem/nanoFramework/StorageEventManager.cs @@ -5,16 +5,20 @@ using nanoFramework.Runtime.Events; using System; -using static nanoFramework.System.IO.FileSystem.RemovableDeviceEventArgs; +using System.Collections; +using System.IO; +using static nanoFramework.System.IO.RemovableDriveEventArgs; -namespace nanoFramework.System.IO.FileSystem +namespace nanoFramework.System.IO { /// /// Provides an event handler that is called when a Removable Device event occurs. /// /// Specifies the object that sent the Removable Device event. /// Contains the Removable Device event arguments. - public delegate void RemovableDeviceEventHandler(Object sender, RemovableDeviceEventArgs e); + public delegate void RemovableDeviceEventHandler( + object sender, + RemovableDriveEventArgs e); /// /// Event manager for Storage events. @@ -32,7 +36,7 @@ internal enum StorageEventType : byte internal class StorageEvent : BaseEvent { public StorageEventType EventType; - public byte DriveIndex; + public uint VolumeIndex; public DateTime Time; } @@ -47,8 +51,8 @@ public BaseEvent ProcessEvent(uint data1, uint data2, DateTime time) { StorageEvent storageEvent = new StorageEvent { - EventType = (StorageEventType)((data1 >> 16) & 0xFF), - DriveIndex = (byte)(data2 & 0xFF), + EventType = (StorageEventType)(data1 & 0xFF), + VolumeIndex = data2, Time = time }; @@ -70,7 +74,7 @@ public bool OnEvent(BaseEvent ev) /// Event that occurs when a Removable Device is inserted. /// /// - /// The class raises events when Removable Devices (typically SD Cards and USB mass storage device) are inserted and removed. + /// The class raises events when Removable Devices (typically SD Cards and USB mass storage device) are inserted and removed. /// /// To have a object call an event-handling method when a event occurs, /// you must associate the method with a delegate, and add this delegate to this event. @@ -81,79 +85,79 @@ public bool OnEvent(BaseEvent ev) /// Event that occurs when a Removable Device is removed. /// /// - /// The class raises events when Removable Devices (typically SD Cards and USB mass storage device) are inserted and removed. + /// The class raises events when Removable Devices (typically SD Cards and USB mass storage device) are inserted and removed. /// /// To have a object call an event-handling method when a event occurs, /// you must associate the method with a delegate, and add this delegate to this event. /// public static event RemovableDeviceEventHandler RemovableDeviceRemoved; + private static ArrayList _drives; + static StorageEventManager() { - StorageEventListener storageEventListener = new StorageEventListener(); + StorageEventListener storageEventListener = new(); EventSink.AddEventProcessor(EventCategory.Storage, storageEventListener); EventSink.AddEventListener(EventCategory.Storage, storageEventListener); + + _drives = new ArrayList(); + + DriveInfo.MountRemovableVolumes(); } internal static void OnStorageEventCallback(StorageEvent storageEvent) { - switch (storageEvent.EventType) + lock (_drives) { - case StorageEventType.RemovableDeviceInsertion: - { - if (RemovableDeviceInserted != null) + switch (storageEvent.EventType) + { + case StorageEventType.RemovableDeviceInsertion: { - RemovableDeviceEventArgs args = new RemovableDeviceEventArgs(DriveIndexToPath(storageEvent.DriveIndex), RemovableDeviceEvent.Inserted); + DriveInfo drive = new(storageEvent.VolumeIndex); + _drives.Add(drive); - RemovableDeviceInserted(null, args); + RemovableDeviceInserted?.Invoke(null, new RemovableDriveEventArgs( + drive, + RemovableDeviceEvent.Inserted)); + break; } - break; - } - case StorageEventType.RemovableDeviceRemoval: - { - if (RemovableDeviceRemoved != null) + + case StorageEventType.RemovableDeviceRemoval: { - RemovableDeviceEventArgs args = new RemovableDeviceEventArgs(DriveIndexToPath(storageEvent.DriveIndex), RemovableDeviceEvent.Removed); + DriveInfo drive = RemoveDrive(storageEvent.VolumeIndex); + + if (drive != null) + { + FileSystemManager.ForceRemoveRootname(drive.Name); + + RemovableDeviceRemoved?.Invoke(null, new RemovableDriveEventArgs(drive, RemovableDeviceEvent.Removed)); + } - RemovableDeviceRemoved(null, args); + break; } + default: break; - } - default: - { - break; - } + } } } - internal static string DriveIndexToPath(byte driveIndex) - { - ///////////////////////////////////////////////////////////////////////////////////// - // Drive indexes have a fixed mapping with a driver letter - // Keep the various INDEX0_DRIVE_LETTER in sync with nanoHAL_Windows_Storage.h in native code - ///////////////////////////////////////////////////////////////////////////////////// - switch (driveIndex) + private static DriveInfo RemoveDrive(uint volumeIndex) + { + for (int i = 0; i < _drives.Count; i++) { - // INDEX0_DRIVE_LETTER - case 0: - return "D:"; - - // INDEX1_DRIVE_LETTER - case 1: - return "E:"; - - // INDEX2_DRIVE_LETTER - case 2: - return "F:"; - - default: -#pragma warning disable S112 // General exceptions should never be thrown - throw new IndexOutOfRangeException(); -#pragma warning restore S112 // General exceptions should never be thrown + DriveInfo drive = (DriveInfo)_drives[i]; + + if (drive._volumeIndex == volumeIndex) + { + _drives.RemoveAt(i); + return drive; + } } + + return null; } } }