Files
dnsmasq-webui/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Validation/DhcpHostSemanticHandler.cs
T
alex 36a160dfc7 Refactor: option semantics to metadata + handlers, shared syntax, registry/save single path
- OptionSemantics: metadata only (no validator delegates); OptionValidationKind + PathPolicy
- IOptionSemanticHandler: handler-first validation; handlers registered via IApplicationMultiSingleton
- OptionSemanticValidator: single engine, handler dispatch then generic kind-based validation
- Shared syntax: DnsmasqScopedDomainSyntax, DnsmasqDhcpRangeValueParser, DnsmasqDhcpOptionSyntax,
  DnsmasqRelaySyntax, DnsmasqDhcpTagSyntax, DnsmasqIpPrefixSyntax for reuse across handlers
- New handlers: Server, Local, RevServer, Address, Alias, Ipset, Nftset, DhcpRange, DhcpHost,
  DhcpOption, DhcpRelay, DhcpProxy, IgnoreAddress, ConnmarkAllowlist, DhcpMatch, DhcpMac,
  PxeService, TrustAnchor, RebindDomainOk, BogusNxdomain, DhcpIgnoreNames, DhcpBoot, Slaac,
  BridgeInterface, SharedNetwork, DhcpOptionPxe, DhcpNameMatch, DhcpIgnore, DhcpVendorclass, DhcpUserclass, TagIf
- Registry and pre-save validation use engine only; placeholders via DnsmasqOptionPlaceholders
- DnsmasqConfigSetService uses DnsmasqDhcpRangeValueParser; DhcpRangeValueParserTests added
2026-03-10 23:44:51 +10:00

102 lines
3.2 KiB
C#

using System.Net;
using System.Text.RegularExpressions;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
/// <summary>
/// Conservative semantic validation for <c>dhcp-host</c> values.
/// Validates obvious token shapes without attempting to fully model every legal dnsmasq variant.
/// </summary>
public sealed partial class DhcpHostSemanticHandler : IOptionSemanticHandler
{
[GeneratedRegex(@"^([0-9A-Fa-f*]{2}:){5}[0-9A-Fa-f*]{2}$", RegexOptions.CultureInvariant)]
private static partial Regex MacPattern();
[GeneratedRegex(@"^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$", RegexOptions.CultureInvariant)]
private static partial Regex HostPattern();
public bool CanHandle(string optionName) =>
optionName == DnsmasqConfKeys.DhcpHost;
public string? ValidateSingle(object? value) => null;
public string? ValidateMultiItem(string? value)
{
var s = value?.Trim() ?? "";
if (s.Length == 0)
return "Value cannot be empty.";
var tokens = s.Split(',').Select(t => t.Trim()).ToArray();
if (tokens.Any(t => t.Length == 0))
return "dhcp-host contains an empty comma-separated segment.";
var hasIdentity = false;
foreach (var token in tokens)
{
if (token.Equals("ignore", StringComparison.OrdinalIgnoreCase) ||
token.StartsWith("set:", StringComparison.OrdinalIgnoreCase) ||
token.StartsWith("tag:", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (token.StartsWith("id:", StringComparison.OrdinalIgnoreCase))
{
hasIdentity = true;
if (token.Length <= 3)
return "dhcp-host id: segment cannot be empty.";
continue;
}
if (MacPattern().IsMatch(token))
{
hasIdentity = true;
continue;
}
if (IsDhcpHostAddress(token) || IsLeaseToken(token))
continue;
if (HostPattern().IsMatch(token))
{
hasIdentity = true;
continue;
}
return $"Unrecognized dhcp-host segment '{token}'.";
}
return hasIdentity
? null
: "dhcp-host must include a MAC address, id:<client-id>, or hostname.";
}
private static bool IsDhcpHostAddress(string value)
{
if (IPAddress.TryParse(value, out _))
return true;
if (value.StartsWith("[", StringComparison.Ordinal) && value.EndsWith("]", StringComparison.Ordinal))
return true;
return false;
}
private static bool IsLeaseToken(string value)
{
if (value.Equals("infinite", StringComparison.OrdinalIgnoreCase))
return true;
if (value.Length == 0)
return false;
var suffix = value[^1];
var number = char.IsLetter(suffix) ? value[..^1] : value;
if (!int.TryParse(number, out _))
return false;
return !char.IsLetter(suffix) || suffix is 's' or 'm' or 'h' or 'd' or 'w';
}
}