mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
- HostEntry.InlineComment; parse at first # on data lines; ToLine + SanitizeInlineComment - HostsPageRow + HostsCache; pending change equality and save diff include comments - Hosts table: dedicated Comment column; edit in own cell; empty shown as em dash - Parser and pending-change tests
125 lines
5.0 KiB
C#
125 lines
5.0 KiB
C#
using DnsmasqWebUI.Models.Hosts;
|
|
using Superpower;
|
|
using Superpower.Model;
|
|
using Superpower.Parsers;
|
|
|
|
namespace DnsmasqWebUI.Infrastructure.Serialization.Parsers.DnsmasqConfig;
|
|
|
|
/// <summary>
|
|
/// Parses a single line of an /etc/hosts-style file. Format: IP_address canonical_hostname [aliases...]
|
|
/// Fields separated by blanks/tabs; text from '#' to EOL is comment (hosts(5), RFC 952).
|
|
/// </summary>
|
|
public static class HostsFileLineParser
|
|
{
|
|
// Token: non-whitespace, non-# (stops at inline comment)
|
|
private static readonly TextParser<string> TokenField =
|
|
Character.Matching(c => !char.IsWhiteSpace(c) && c != '#', "token")
|
|
.AtLeastOnce().Text();
|
|
|
|
// Optional leading # (comment line); when present, rest of line is comment text, not data
|
|
private static readonly TextParser<bool> OptionalComment =
|
|
ConfParserHelpers.Token(Character.EqualTo('#')).Select(_ => true).OptionalOrDefault(false);
|
|
|
|
// Content: address (first token) then one or more names (tokens). Per hosts(5): IP then canonical name [aliases...].
|
|
private static readonly TextParser<(string address, List<string> names)> Content =
|
|
from address in TokenField
|
|
from _ in Span.WhiteSpace
|
|
from names in TokenField.AtLeastOnce()
|
|
select (address, names.ToList());
|
|
|
|
// Full line: if starts with #, treat entire line as comment (do not parse rest as address/names); else parse Content.
|
|
private static readonly TextParser<(bool isComment, string address, List<string> names)> LineContent =
|
|
OptionalComment.Then(hasComment =>
|
|
hasComment
|
|
? Character.AnyChar.Many().Text().Select(_ => (true, "", new List<string>()))
|
|
: Content.Select(c => (false, c.address, c.names)));
|
|
|
|
public static HostEntry? ParseLine(string line, int lineNumber)
|
|
{
|
|
var trimmed = line.Trim();
|
|
if (string.IsNullOrEmpty(trimmed))
|
|
return new HostEntry { LineNumber = lineNumber, RawLine = line, IsPassthrough = true };
|
|
|
|
if (trimmed.StartsWith('#'))
|
|
{
|
|
var result = LineContent.TryParse(trimmed);
|
|
if (!result.HasValue)
|
|
return new HostEntry { LineNumber = lineNumber, RawLine = line, IsPassthrough = true };
|
|
|
|
var (isComment, address, names) = result.Value;
|
|
if (string.IsNullOrEmpty(address) && names.Count == 0)
|
|
return new HostEntry { LineNumber = lineNumber, RawLine = line, IsComment = isComment, IsPassthrough = true };
|
|
|
|
return new HostEntry
|
|
{
|
|
LineNumber = lineNumber,
|
|
Address = address,
|
|
Names = names,
|
|
RawLine = line,
|
|
IsComment = isComment
|
|
};
|
|
}
|
|
|
|
string? inlineComment = null;
|
|
var dataPart = trimmed;
|
|
var hashIdx = trimmed.IndexOf('#');
|
|
if (hashIdx >= 0)
|
|
{
|
|
inlineComment = hashIdx + 1 < trimmed.Length ? trimmed[(hashIdx + 1)..].Trim() : null;
|
|
if (string.IsNullOrEmpty(inlineComment))
|
|
inlineComment = null;
|
|
dataPart = trimmed[..hashIdx].TrimEnd();
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(dataPart))
|
|
return new HostEntry { LineNumber = lineNumber, RawLine = line, IsPassthrough = true };
|
|
|
|
var dataResult = LineContent.TryParse(dataPart);
|
|
if (!dataResult.HasValue)
|
|
return new HostEntry { LineNumber = lineNumber, RawLine = line, IsPassthrough = true };
|
|
|
|
var (isCommentData, addressData, namesData) = dataResult.Value;
|
|
if (string.IsNullOrEmpty(addressData) && namesData.Count == 0)
|
|
return new HostEntry
|
|
{
|
|
LineNumber = lineNumber,
|
|
RawLine = line,
|
|
IsComment = isCommentData,
|
|
IsPassthrough = true,
|
|
InlineComment = inlineComment
|
|
};
|
|
|
|
return new HostEntry
|
|
{
|
|
LineNumber = lineNumber,
|
|
Address = addressData,
|
|
Names = namesData,
|
|
RawLine = line,
|
|
IsComment = isCommentData,
|
|
InlineComment = inlineComment
|
|
};
|
|
}
|
|
|
|
public static string ToLine(HostEntry entry)
|
|
{
|
|
if (entry.IsPassthrough)
|
|
return entry.RawLine;
|
|
var prefix = entry.IsComment ? "# " : "";
|
|
var core = prefix + entry.Address + " " + string.Join(" ", entry.Names);
|
|
var sanitized = SanitizeInlineComment(entry.InlineComment);
|
|
if (sanitized != null)
|
|
return core.TrimEnd() + " # " + sanitized;
|
|
return core;
|
|
}
|
|
|
|
/// <summary>Strip characters that would break a hosts line; drop <c>#</c> (comment delimiter).</summary>
|
|
public static string? SanitizeInlineComment(string? text)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
return null;
|
|
var flattened = text.Replace('\r', ' ').Replace('\n', ' ').Replace('#', ' ');
|
|
var collapsed = string.Join(' ', flattened.Split(' ', StringSplitOptions.RemoveEmptyEntries)).Trim();
|
|
return collapsed.Length == 0 ? null : collapsed;
|
|
}
|
|
}
|