Files
dnsmasq-webui/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Validation/DnsmasqScopedDomainSyntax.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

68 lines
2.3 KiB
C#

using System.Text.RegularExpressions;
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
/// <summary>
/// Shared parsing/validation helpers for dnsmasq option values that use the
/// <c>/domain[/domain...]/tail</c> syntax shared by server/local/address/ipset/nftset.
/// </summary>
internal static partial class DnsmasqScopedDomainSyntax
{
[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 DomainPattern();
public static bool TrySplitScopedValue(string value, out string[] domains, out string tail, out string? error)
{
domains = Array.Empty<string>();
tail = "";
error = null;
if (!value.StartsWith("/", StringComparison.Ordinal))
{
error = "Value must start with '/'.";
return false;
}
var lastSlash = value.LastIndexOf('/');
if (lastSlash <= 0)
{
error = "Value must use /domain[/domain...]/... syntax.";
return false;
}
domains = value[1..lastSlash].Split('/');
tail = value[(lastSlash + 1)..];
return true;
}
public static string? ValidateDomainPatterns(IEnumerable<string> domains, bool allowUnqualifiedMarker = true, bool allowHash = true)
{
var list = domains.ToArray();
if (list.Length == 0)
return "Value must include at least one domain pattern.";
if (allowUnqualifiedMarker && list.Length == 1 && list[0].Length == 0)
return null; // "//" means unqualified names only
foreach (var domain in list)
{
if (string.IsNullOrWhiteSpace(domain))
return "Value contains an empty domain pattern.";
if (allowHash && domain == "#")
continue;
var normalized = domain;
if (normalized.StartsWith("*", StringComparison.Ordinal))
normalized = normalized[1..];
if (normalized.StartsWith(".", StringComparison.Ordinal))
normalized = normalized[1..];
if (normalized.Length == 0 || normalized.Length > 253 || !DomainPattern().IsMatch(normalized))
return $"Invalid domain pattern '{domain}'.";
}
return null;
}
}