[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("host 10.1.1.1")] [DataRow("host 10.1.1.1")]
[DataRow("# comment 10.1.1.1 host # comment")] [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) public void Not_Valid_Entry(string line)
{ {
var entry = new Entry(0, 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); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); 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)); 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); var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult); Assert.AreEqual(result.TextContents, contentResult);
@@ -94,9 +95,10 @@ namespace Hosts.Tests
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); 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); entries.RemoveAt(0);
await service.WriteAsync(string.Empty, entries); await service.WriteAsync(data.AdditionalLines, entries);
var result = fileSystem.GetFile(service.HostsFilePath); var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult); Assert.AreEqual(result.TextContents, contentResult);
@@ -120,13 +122,13 @@ namespace Hosts.Tests
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (_, entries) = await service.ReadAsync(); var data = await service.ReadAsync();
var entry = entries[0]; var entry = data.Entries[0];
entry.Address = "10.1.1.10"; entry.Address = "10.1.1.10";
entry.Hosts = "host host.local host1.local"; entry.Hosts = "host host.local host1.local";
entry.Comment = "updated comment"; entry.Comment = "updated comment";
entry.Active = false; entry.Active = false;
await service.WriteAsync(string.Empty, entries); await service.WriteAsync(data.AdditionalLines, data.Entries);
var result = fileSystem.GetFile(service.HostsFilePath); var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult); Assert.AreEqual(result.TextContents, contentResult);
@@ -172,8 +174,8 @@ namespace Hosts.Tests
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (additionalLines, entries) = await service.ReadAsync(); var data = await service.ReadAsync();
await service.WriteAsync(additionalLines, entries); await service.WriteAsync(data.AdditionalLines, data.Entries);
var result = fileSystem.GetFile(service.HostsFilePath); var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult); Assert.AreEqual(result.TextContents, contentResult);
@@ -205,8 +207,33 @@ namespace Hosts.Tests
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var (additionalLines, entries) = await service.ReadAsync(); var data = await service.ReadAsync();
await service.WriteAsync(additionalLines, entries); 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); var result = fileSystem.GetFile(service.HostsFilePath);
Assert.AreEqual(result.TextContents, contentResult); 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); 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 entries = new List<Entry>();
var unparsedBuilder = new StringBuilder(); var unparsedBuilder = new StringBuilder();
var splittedEntries = false;
if (!Exists()) if (!Exists())
{ {
return (unparsedBuilder.ToString(), entries); return new HostsData(entries, unparsedBuilder.ToString(), false);
} }
var lines = await _fileSystem.File.ReadAllLinesAsync(HostsFilePath, Encoding); var lines = await _fileSystem.File.ReadAllLinesAsync(HostsFilePath, Encoding);
var id = 0;
for (var i = 0; i < lines.Length; i++) for (var i = 0; i < lines.Length; i++)
{ {
var line = lines[i]; var line = lines[i];
@@ -85,11 +88,25 @@ namespace Hosts.Helpers
continue; continue;
} }
var entry = new Entry(i, line); var entry = new Entry(id, line);
if (entry.Valid) if (entry.Valid)
{ {
entries.Add(entry); 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 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) public async Task<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries)

View File

@@ -15,7 +15,7 @@ namespace Hosts.Helpers
event EventHandler FileChanged; event EventHandler FileChanged;
Task<(string Unparsed, List<Entry> Entries)> ReadAsync(); Task<HostsData> ReadAsync();
Task<bool> WriteAsync(string additionalLines, IEnumerable<Entry> entries); 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. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Hosts.Helpers namespace Hosts.Helpers
@@ -39,16 +40,23 @@ namespace Hosts.Helpers
/// <summary> /// <summary>
/// Determines whether the hosts are valid /// Determines whether the hosts are valid
/// </summary> /// </summary>
public static bool ValidHosts(string hosts) public static bool ValidHosts(string hosts, bool validateHostsLength)
{ {
if (string.IsNullOrWhiteSpace(hosts)) if (string.IsNullOrWhiteSpace(hosts))
{ {
return false; 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; return false;
} }

View File

@@ -58,7 +58,7 @@ namespace Hosts.Models
[ObservableProperty] [ObservableProperty]
private bool _duplicate; private bool _duplicate;
public bool Valid => ValidationHelper.ValidHosts(Hosts) && Type != AddressType.Invalid; public bool Valid => Validate(true);
public string Line { get; private set; } public string Line { get; private set; }
@@ -150,5 +150,10 @@ namespace Hosts.Models
Active = Active, 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> <comment>"Hosts" refers to the system hosts file, do not loc</comment>
</data> </data>
<data name="Hosts.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve"> <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> <comment>Do not localize "server" and "server.local"</comment>
</data> </data>
<data name="HostsFilter.Header" xml:space="preserve"> <data name="HostsFilter.Header" xml:space="preserve">
@@ -272,6 +272,17 @@
<data name="ShowOnlyDuplicates.Header" xml:space="preserve"> <data name="ShowOnlyDuplicates.Header" xml:space="preserve">
<value>Show only duplicates</value> <value>Show only duplicates</value>
</data> </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"> <data name="UpdateBtn" xml:space="preserve">
<value>Update</value> <value>Update</value>
</data> </data>

View File

@@ -68,6 +68,9 @@ namespace Hosts.ViewModels
[ObservableProperty] [ObservableProperty]
private bool _showOnlyDuplicates; private bool _showOnlyDuplicates;
[ObservableProperty]
private bool _showSplittedEntriesTooltip;
partial void OnShowOnlyDuplicatesChanged(bool value) partial void OnShowOnlyDuplicatesChanged(bool value)
{ {
ApplyFilters(); ApplyFilters();
@@ -164,12 +167,13 @@ namespace Hosts.ViewModels
Task.Run(async () => Task.Run(async () =>
{ {
_readingHosts = true; _readingHosts = true;
var (additionalLines, entries) = await _hostsService.ReadAsync(); var data = await _hostsService.ReadAsync();
await _dispatcherQueue.EnqueueAsync(() => await _dispatcherQueue.EnqueueAsync(() =>
{ {
AdditionalLines = additionalLines; ShowSplittedEntriesTooltip = data.SplittedEntries;
_entries = new ObservableCollection<Entry>(entries); AdditionalLines = data.AdditionalLines;
_entries = new ObservableCollection<Entry>(data.Entries);
foreach (var e in _entries) foreach (var e in _entries)
{ {
@@ -346,12 +350,28 @@ namespace Hosts.ViewModels
return; 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 && e.Type == entry.Type
&& (string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase) && entry.SplittedHosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any()))
|| hosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any())) != null; {
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(() => _dispatcherQueue.TryEnqueue(() =>
{ {

View File

@@ -435,7 +435,7 @@
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
HorizontalScrollMode="Auto"> HorizontalScrollMode="Auto">
<StackPanel <StackPanel
MinWidth="480" Width="480"
Margin="0,12,0,0" Margin="0,12,0,0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Spacing="24"> Spacing="24">
@@ -446,10 +446,22 @@
<TextBox <TextBox
x:Uid="Hosts" x:Uid="Hosts"
IsSpellCheckEnabled="False" IsSpellCheckEnabled="False"
AcceptsReturn="False"
TextWrapping="Wrap"
Height="100"
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
Text="{Binding Hosts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> Text="{Binding Hosts, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBox <TextBox
x:Uid="Comment" 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}" /> Text="{Binding Comment, Mode=TwoWay}" />
<ToggleSwitch <ToggleSwitch
x:Uid="Active" x:Uid="Active"
@@ -486,5 +498,20 @@
ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.VerticalScrollMode="Enabled"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</ContentDialog> </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> </Grid>
</Page> </Page>