diff --git a/src/modules/Hosts/Hosts.Tests/EntryTest.cs b/src/modules/Hosts/Hosts.Tests/EntryTest.cs index 501294725a..505f4d8474 100644 --- a/src/modules/Hosts/Hosts.Tests/EntryTest.cs +++ b/src/modules/Hosts/Hosts.Tests/EntryTest.cs @@ -75,6 +75,7 @@ namespace Hosts.Tests [DataRow(" host 10.1.1.1")] [DataRow("host 10.1.1.1")] [DataRow("# comment 10.1.1.1 host # comment")] + [DataRow("10.1.1.1 host01 host02 host03 host04 host05 host06 host07 host08 host09 host10")] public void Not_Valid_Entry(string line) { var entry = new Entry(0, line); diff --git a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs index 19b017eff4..268de099f7 100644 --- a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs +++ b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs @@ -69,9 +69,10 @@ namespace Hosts.Tests var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); - var (_, entries) = await service.ReadAsync(); + var data = await service.ReadAsync(); + var entries = data.Entries.ToList(); entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false)); - await service.WriteAsync(string.Empty, entries); + await service.WriteAsync(data.AdditionalLines, entries); var result = fileSystem.GetFile(service.HostsFilePath); Assert.AreEqual(result.TextContents, contentResult); @@ -94,9 +95,10 @@ namespace Hosts.Tests var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); - var (_, entries) = await service.ReadAsync(); + var data = await service.ReadAsync(); + var entries = data.Entries.ToList(); entries.RemoveAt(0); - await service.WriteAsync(string.Empty, entries); + await service.WriteAsync(data.AdditionalLines, entries); var result = fileSystem.GetFile(service.HostsFilePath); Assert.AreEqual(result.TextContents, contentResult); @@ -120,13 +122,13 @@ namespace Hosts.Tests var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); - var (_, entries) = await service.ReadAsync(); - var entry = entries[0]; + var data = await service.ReadAsync(); + var entry = data.Entries[0]; entry.Address = "10.1.1.10"; entry.Hosts = "host host.local host1.local"; entry.Comment = "updated comment"; entry.Active = false; - await service.WriteAsync(string.Empty, entries); + await service.WriteAsync(data.AdditionalLines, data.Entries); var result = fileSystem.GetFile(service.HostsFilePath); Assert.AreEqual(result.TextContents, contentResult); @@ -172,8 +174,8 @@ namespace Hosts.Tests var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); - var (additionalLines, entries) = await service.ReadAsync(); - await service.WriteAsync(additionalLines, entries); + var data = await service.ReadAsync(); + await service.WriteAsync(data.AdditionalLines, data.Entries); var result = fileSystem.GetFile(service.HostsFilePath); Assert.AreEqual(result.TextContents, contentResult); @@ -205,8 +207,33 @@ namespace Hosts.Tests var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); - var (additionalLines, entries) = await service.ReadAsync(); - await service.WriteAsync(additionalLines, entries); + var data = await service.ReadAsync(); + await service.WriteAsync(data.AdditionalLines, data.Entries); + + var result = fileSystem.GetFile(service.HostsFilePath); + Assert.AreEqual(result.TextContents, contentResult); + } + + [TestMethod] + public async Task LongHosts_Splitted() + { + var content = +@"10.1.1.1 host01 host02 host03 host04 host05 host06 host07 host08 host09 host10 host11 host12 host13 host14 host15 host16 host17 host18 host19 # comment +"; + + var contentResult = +@"10.1.1.1 host01 host02 host03 host04 host05 host06 host07 host08 host09 # comment +10.1.1.1 host10 host11 host12 host13 host14 host15 host16 host17 host18 # comment +10.1.1.1 host19 # comment +"; + + var fileSystem = new CustomMockFileSystem(); + var userSettings = new Mock(); + var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); + fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); + + var data = await service.ReadAsync(); + await service.WriteAsync(data.AdditionalLines, data.Entries); var result = fileSystem.GetFile(service.HostsFilePath); Assert.AreEqual(result.TextContents, contentResult); diff --git a/src/modules/Hosts/Hosts/Consts.cs b/src/modules/Hosts/Hosts/Consts.cs new file mode 100644 index 0000000000..3772f34279 --- /dev/null +++ b/src/modules/Hosts/Hosts/Consts.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Hosts +{ + public static class Consts + { + /// + /// Maximum number of hosts detected by the system in a single line + /// + public const int MaxHostsCount = 9; + } +} diff --git a/src/modules/Hosts/Hosts/Helpers/HostsService.cs b/src/modules/Hosts/Hosts/Helpers/HostsService.cs index 58629c404f..ed698ea7af 100644 --- a/src/modules/Hosts/Hosts/Helpers/HostsService.cs +++ b/src/modules/Hosts/Hosts/Helpers/HostsService.cs @@ -64,18 +64,21 @@ namespace Hosts.Helpers return _fileSystem.File.Exists(HostsFilePath); } - public async Task<(string Unparsed, List Entries)> ReadAsync() + public async Task ReadAsync() { var entries = new List(); var unparsedBuilder = new StringBuilder(); + var splittedEntries = false; if (!Exists()) { - return (unparsedBuilder.ToString(), entries); + return new HostsData(entries, unparsedBuilder.ToString(), false); } var lines = await _fileSystem.File.ReadAllLinesAsync(HostsFilePath, Encoding); + var id = 0; + for (var i = 0; i < lines.Length; i++) { var line = lines[i]; @@ -85,11 +88,25 @@ namespace Hosts.Helpers continue; } - var entry = new Entry(i, line); + var entry = new Entry(id, line); if (entry.Valid) { entries.Add(entry); + id++; + } + else if (entry.Validate(false)) + { + foreach (var hostsChunk in entry.SplittedHosts.Chunk(Consts.MaxHostsCount)) + { + var clonedEntry = entry.Clone(); + clonedEntry.Id = id; + clonedEntry.Hosts = string.Join(' ', hostsChunk); + entries.Add(clonedEntry); + id++; + } + + splittedEntries = true; } else { @@ -102,7 +119,7 @@ namespace Hosts.Helpers } } - return (unparsedBuilder.ToString(), entries); + return new HostsData(entries, unparsedBuilder.ToString(), splittedEntries); } public async Task WriteAsync(string additionalLines, IEnumerable entries) diff --git a/src/modules/Hosts/Hosts/Helpers/IHostsService.cs b/src/modules/Hosts/Hosts/Helpers/IHostsService.cs index 06ef700914..71e3e4b727 100644 --- a/src/modules/Hosts/Hosts/Helpers/IHostsService.cs +++ b/src/modules/Hosts/Hosts/Helpers/IHostsService.cs @@ -15,7 +15,7 @@ namespace Hosts.Helpers event EventHandler FileChanged; - Task<(string Unparsed, List Entries)> ReadAsync(); + Task ReadAsync(); Task WriteAsync(string additionalLines, IEnumerable entries); diff --git a/src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs b/src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs index d93c9438ef..6a10772299 100644 --- a/src/modules/Hosts/Hosts/Helpers/ValidationHelper.cs +++ b/src/modules/Hosts/Hosts/Helpers/ValidationHelper.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; using System.Text.RegularExpressions; namespace Hosts.Helpers @@ -39,16 +40,23 @@ namespace Hosts.Helpers /// /// Determines whether the hosts are valid /// - public static bool ValidHosts(string hosts) + public static bool ValidHosts(string hosts, bool validateHostsLength) { if (string.IsNullOrWhiteSpace(hosts)) { return false; } - foreach (var host in hosts.Split(' ')) + var splittedHosts = hosts.Split(' '); + + if (validateHostsLength && splittedHosts.Length > Consts.MaxHostsCount) { - if (System.Uri.CheckHostName(host) == System.UriHostNameType.Unknown) + return false; + } + + foreach (var host in splittedHosts) + { + if (Uri.CheckHostName(host) == UriHostNameType.Unknown) { return false; } diff --git a/src/modules/Hosts/Hosts/Models/Entry.cs b/src/modules/Hosts/Hosts/Models/Entry.cs index dfce3c3cb1..7e8fa0a5dc 100644 --- a/src/modules/Hosts/Hosts/Models/Entry.cs +++ b/src/modules/Hosts/Hosts/Models/Entry.cs @@ -58,7 +58,7 @@ namespace Hosts.Models [ObservableProperty] private bool _duplicate; - public bool Valid => ValidationHelper.ValidHosts(Hosts) && Type != AddressType.Invalid; + public bool Valid => Validate(true); public string Line { get; private set; } @@ -150,5 +150,10 @@ namespace Hosts.Models Active = Active, }; } + + public bool Validate(bool validateHostsLength) + { + return Type != AddressType.Invalid && ValidationHelper.ValidHosts(Hosts, validateHostsLength); + } } } diff --git a/src/modules/Hosts/Hosts/Models/HostsData.cs b/src/modules/Hosts/Hosts/Models/HostsData.cs new file mode 100644 index 0000000000..abb79a69ef --- /dev/null +++ b/src/modules/Hosts/Hosts/Models/HostsData.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// 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.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Hosts.Models +{ + /// + /// Represents the parsed hosts file + /// + public class HostsData + { + /// + /// Gets the parsed entries + /// + public ReadOnlyCollection Entries { get; } + + /// + /// Gets the lines that couldn't be parsed + /// + public string AdditionalLines { get; } + + /// + /// Gets a value indicating whether some entries been splitted + /// + public bool SplittedEntries { get; } + + public HostsData(List entries, string additionalLines, bool splittedEntries) + { + Entries = entries.AsReadOnly(); + AdditionalLines = additionalLines; + SplittedEntries = splittedEntries; + } + } +} diff --git a/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw b/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw index 4fb9069429..7023bdd5e7 100644 --- a/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw +++ b/src/modules/Hosts/Hosts/Strings/en-us/Resources.resw @@ -231,7 +231,7 @@ "Hosts" refers to the system hosts file, do not loc - Seperate multiple hosts by space (e.g. server server.local). + Seperate multiple hosts by space (e.g. server server.local). Maximum 9 hosts per entry. Do not localize "server" and "server.local" @@ -272,6 +272,17 @@ Show only duplicates + + Only 9 hosts per entry are supported + "Hosts" refers to the system hosts file, do not loc + + + Entries with too many hosts found + "Hosts" refers to the system hosts file, do not loc + + + The affected entries have been splitted. This will take effect on next change. + Update diff --git a/src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs b/src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs index 7327b10310..6b5f1101ed 100644 --- a/src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs +++ b/src/modules/Hosts/Hosts/ViewModels/MainViewModel.cs @@ -68,6 +68,9 @@ namespace Hosts.ViewModels [ObservableProperty] private bool _showOnlyDuplicates; + [ObservableProperty] + private bool _showSplittedEntriesTooltip; + partial void OnShowOnlyDuplicatesChanged(bool value) { ApplyFilters(); @@ -164,12 +167,13 @@ namespace Hosts.ViewModels Task.Run(async () => { _readingHosts = true; - var (additionalLines, entries) = await _hostsService.ReadAsync(); + var data = await _hostsService.ReadAsync(); await _dispatcherQueue.EnqueueAsync(() => { - AdditionalLines = additionalLines; - _entries = new ObservableCollection(entries); + ShowSplittedEntriesTooltip = data.SplittedEntries; + AdditionalLines = data.AdditionalLines; + _entries = new ObservableCollection(data.Entries); foreach (var e in _entries) { @@ -346,12 +350,28 @@ namespace Hosts.ViewModels return; } - var hosts = entry.SplittedHosts; + var duplicate = false; - var duplicate = _entries.FirstOrDefault(e => e != entry + /* + * Duplicate are based on the following criteria: + * Entries with the same type and at least one host in common + * Entries with the same type and address, except when there is only one entry with less than 9 hosts for that type and address + */ + if (_entries.Any(e => e != entry && e.Type == entry.Type - && (string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase) - || hosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any())) != null; + && entry.SplittedHosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any())) + { + duplicate = true; + } + else if (_entries.Any(e => e != entry + && e.Type == entry.Type + && string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase))) + { + duplicate = entry.SplittedHosts.Length < Consts.MaxHostsCount + && _entries.Count(e => e.Type == entry.Type + && string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase) + && e.SplittedHosts.Length < Consts.MaxHostsCount) > 1; + } _dispatcherQueue.TryEnqueue(() => { diff --git a/src/modules/Hosts/Hosts/Views/MainPage.xaml b/src/modules/Hosts/Hosts/Views/MainPage.xaml index 4390538e09..1ed9c48848 100644 --- a/src/modules/Hosts/Hosts/Views/MainPage.xaml +++ b/src/modules/Hosts/Hosts/Views/MainPage.xaml @@ -435,7 +435,7 @@ HorizontalScrollBarVisibility="Auto" HorizontalScrollMode="Auto"> @@ -446,10 +446,22 @@ + + + + + + + + +