diff --git a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs index 1eeef3b752..d766c80f41 100644 --- a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs +++ b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; @@ -248,25 +249,53 @@ namespace Hosts.Tests { var fileSystem = new CustomMockFileSystem(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); - var hostsFile = new MockFileData(string.Empty); - hostsFile.Attributes = System.IO.FileAttributes.ReadOnly; + + var hostsFile = new MockFileData(string.Empty) + { + Attributes = FileAttributes.ReadOnly, + }; + fileSystem.AddFile(service.HostsFilePath, hostsFile); await Assert.ThrowsExceptionAsync(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty())); } [TestMethod] - public void Remove_ReadOnly() + public void Remove_ReadOnly_Attribute() { var fileSystem = new CustomMockFileSystem(); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); - var hostsFile = new MockFileData(string.Empty); - hostsFile.Attributes = System.IO.FileAttributes.ReadOnly; + + var hostsFile = new MockFileData(string.Empty) + { + Attributes = FileAttributes.ReadOnly, + }; + fileSystem.AddFile(service.HostsFilePath, hostsFile); - service.RemoveReadOnly(); - var readOnly = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(System.IO.FileAttributes.ReadOnly); + service.RemoveReadOnlyAttribute(); + + var readOnly = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(FileAttributes.ReadOnly); Assert.IsFalse(readOnly); } + + [TestMethod] + public async Task Save_Hidden_Hosts() + { + var fileSystem = new CustomMockFileSystem(); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + + var hostsFile = new MockFileData(string.Empty) + { + Attributes = FileAttributes.Hidden, + }; + + fileSystem.AddFile(service.HostsFilePath, hostsFile); + + await service.WriteAsync("# Empty hosts file", Enumerable.Empty()); + + var hidden = fileSystem.FileInfo.FromFileName(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden); + Assert.IsTrue(hidden); + } } } diff --git a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs index 08fbde86c8..02fb08bec2 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs @@ -23,6 +23,7 @@ namespace HostsUILib.Helpers public class HostsService : IHostsService, IDisposable { private const string _backupSuffix = $"_PowerToysBackup_"; + private const int _defaultBufferSize = 4096; // From System.IO.File source code private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1); private readonly IFileSystem _fileSystem; @@ -197,7 +198,16 @@ namespace HostsUILib.Helpers _backupDone = true; } - await _fileSystem.File.WriteAllLinesAsync(HostsFilePath, lines, Encoding); + // FileMode.OpenOrCreate is necessary to prevent UnauthorizedAccessException when the hosts file is hidden + using var stream = _fileSystem.FileStream.Create(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, _defaultBufferSize, FileOptions.Asynchronous); + using var writer = new StreamWriter(stream, Encoding); + foreach (var line in lines) + { + await writer.WriteLineAsync(line.AsMemory()); + } + + stream.SetLength(stream.Position); + await writer.FlushAsync(); } finally { @@ -292,7 +302,7 @@ namespace HostsUILib.Helpers } } - public void RemoveReadOnly() + public void RemoveReadOnlyAttribute() { var fileInfo = _fileSystem.FileInfo.FromFileName(HostsFilePath); if (fileInfo.IsReadOnly) diff --git a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs index 955c1f8f8c..06ea152ea3 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs @@ -25,6 +25,6 @@ namespace HostsUILib.Helpers void OpenHostsFile(); - void RemoveReadOnly(); + void RemoveReadOnlyAttribute(); } } diff --git a/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs b/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs index 482bc7e1dd..aef42fbc5a 100644 --- a/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs +++ b/src/modules/Hosts/HostsUILib/ViewModels/MainViewModel.cs @@ -283,7 +283,7 @@ namespace HostsUILib.ViewModels [RelayCommand] public void OverwriteHosts() { - _hostsService.RemoveReadOnly(); + _hostsService.RemoveReadOnlyAttribute(); _ = Task.Run(SaveAsync); }