[Hosts] Handle maximum of 9 hosts per entry (#26862)

* handle maximum of 9 hosts per entry

* splitted entries teaching tip

* fix entry

* message changed
This commit is contained in:
Davide Giacometti
2023-06-23 21:54:45 +02:00
committed by GitHub
parent 8cb632a0c2
commit 9511d17063
11 changed files with 197 additions and 30 deletions

View File

@@ -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);

View File

@@ -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<IUserSettings>();
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);

View File

@@ -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
{
/// <summary>
/// Maximum number of hosts detected by the system in a single line
/// </summary>
public const int MaxHostsCount = 9;
}
}

View File

@@ -64,18 +64,21 @@ namespace Hosts.Helpers
return _fileSystem.File.Exists(HostsFilePath);
}
public async Task<(string Unparsed, List<Entry> Entries)> ReadAsync()
public async Task<HostsData> ReadAsync()
{
var entries = new List<Entry>();
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<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries)

View File

@@ -15,7 +15,7 @@ namespace Hosts.Helpers
event EventHandler FileChanged;
Task<(string Unparsed, List<Entry> Entries)> ReadAsync();
Task<HostsData> ReadAsync();
Task<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries);

View File

@@ -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
/// <summary>
/// Determines whether the hosts are valid
/// </summary>
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;
}

View File

@@ -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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents the parsed hosts file
/// </summary>
public class HostsData
{
/// <summary>
/// Gets the parsed entries
/// </summary>
public ReadOnlyCollection<Entry> Entries { get; }
/// <summary>
/// Gets the lines that couldn't be parsed
/// </summary>
public string AdditionalLines { get; }
/// <summary>
/// Gets a value indicating whether some entries been splitted
/// </summary>
public bool SplittedEntries { get; }
public HostsData(List<Entry> entries, string additionalLines, bool splittedEntries)
{
Entries = entries.AsReadOnly();
AdditionalLines = additionalLines;
SplittedEntries = splittedEntries;
}
}
}

View File

@@ -231,7 +231,7 @@
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="Hosts.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Seperate multiple hosts by space (e.g. server server.local).</value>
<value>Seperate multiple hosts by space (e.g. server server.local). Maximum 9 hosts per entry.</value>
<comment>Do not localize "server" and "server.local"</comment>
</data>
<data name="HostsFilter.Header" xml:space="preserve">
@@ -272,6 +272,17 @@
<data name="ShowOnlyDuplicates.Header" xml:space="preserve">
<value>Show only duplicates</value>
</data>
<data name="TooManyHostsTeachingTip.Subtitle" xml:space="preserve">
<value>Only 9 hosts per entry are supported</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="TooManyHostsTeachingTip.Title" xml:space="preserve">
<value>Entries with too many hosts found</value>
<comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data>
<data name="TooManyHostsTeachingTipContent.Text" xml:space="preserve">
<value>The affected entries have been splitted. This will take effect on next change.</value>
</data>
<data name="UpdateBtn" xml:space="preserve">
<value>Update</value>
</data>

View File

@@ -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<Entry>(entries);
ShowSplittedEntriesTooltip = data.SplittedEntries;
AdditionalLines = data.AdditionalLines;
_entries = new ObservableCollection<Entry>(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(() =>
{

View File

@@ -435,7 +435,7 @@
HorizontalScrollBarVisibility="Auto"
HorizontalScrollMode="Auto">
<StackPanel
MinWidth="480"
Width="480"
Margin="0,12,0,0"
HorizontalAlignment="Stretch"
Spacing="24">
@@ -446,10 +446,22 @@
<TextBox
x:Uid="Hosts"
IsSpellCheckEnabled="False"
AcceptsReturn="False"
TextWrapping="Wrap"
Height="100"
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
Text="{Binding Hosts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBox
x:Uid="Comment"
IsSpellCheckEnabled="False"
IsSpellCheckEnabled="False"
AcceptsReturn="False"
TextWrapping="Wrap"
Height="100"
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
Text="{Binding Comment, Mode=TwoWay}" />
<ToggleSwitch
x:Uid="Active"
@@ -486,5 +498,20 @@
ScrollViewer.VerticalScrollMode="Enabled"
TextWrapping="Wrap" />
</ContentDialog>
<TeachingTip
x:Uid="TooManyHostsTeachingTip"
IsOpen="{x:Bind ViewModel.ShowSplittedEntriesTooltip, Mode=OneWay}"
PreferredPlacement="Top"
PlacementMargin="20">
<TeachingTip.IconSource>
<FontIconSource Glyph="&#xe946;" />
</TeachingTip.IconSource>
<TeachingTip.Content>
<TextBlock x:Uid="TooManyHostsTeachingTipContent"
TextWrapping="Wrap"
Margin="0,16,0,0" />
</TeachingTip.Content>
</TeachingTip>
</Grid>
</Page>