From 3ec78dd38a801434e1313814c692a056a5a2b1dc Mon Sep 17 00:00:00 2001 From: wwmmzz <270447914@qq.com> Date: Sun, 24 May 2026 13:30:54 +0800 Subject: [PATCH] Allow numeric routing ports --- .../Fmt/RoutingRulePortConverterTests.cs | 48 +++++++++++++++++++ .../Common/JsonStringOrNumberConverter.cs | 45 +++++++++++++++++ .../ServiceLib/Models/Entities/RulesItem.cs | 1 + 3 files changed, 94 insertions(+) create mode 100644 v2rayN/ServiceLib.Tests/Fmt/RoutingRulePortConverterTests.cs create mode 100644 v2rayN/ServiceLib/Common/JsonStringOrNumberConverter.cs diff --git a/v2rayN/ServiceLib.Tests/Fmt/RoutingRulePortConverterTests.cs b/v2rayN/ServiceLib.Tests/Fmt/RoutingRulePortConverterTests.cs new file mode 100644 index 00000000..39ecc63d --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Fmt/RoutingRulePortConverterTests.cs @@ -0,0 +1,48 @@ +using AwesomeAssertions; +using ServiceLib.Models.Entities; +using Xunit; + +namespace ServiceLib.Tests.Fmt; + +public class RoutingRulePortConverterTests +{ + [Fact] + public void DeserializeRoutingRules_PortNumber_ShouldBeAccepted() + { + var json = """ + [ + { + "type": "field", + "outboundTag": "direct", + "port": 53 + } + ] + """; + + var rules = JsonUtils.Deserialize>(json); + + rules.Should().NotBeNull(); + rules!.Should().HaveCount(1); + rules[0].Port.Should().Be("53"); + } + + [Fact] + public void DeserializeRoutingRules_PortStringRange_ShouldStillWork() + { + var json = """ + [ + { + "type": "field", + "outboundTag": "proxy", + "port": "0-65535" + } + ] + """; + + var rules = JsonUtils.Deserialize>(json); + + rules.Should().NotBeNull(); + rules!.Should().HaveCount(1); + rules[0].Port.Should().Be("0-65535"); + } +} diff --git a/v2rayN/ServiceLib/Common/JsonStringOrNumberConverter.cs b/v2rayN/ServiceLib/Common/JsonStringOrNumberConverter.cs new file mode 100644 index 00000000..882a7c45 --- /dev/null +++ b/v2rayN/ServiceLib/Common/JsonStringOrNumberConverter.cs @@ -0,0 +1,45 @@ +namespace ServiceLib.Common; + +/// +/// Accepts either a JSON string or number and stores the value as a string. +/// This keeps existing routing rule storage intact while allowing numeric clipboard input. +/// +public sealed class JsonStringOrNumberConverter : JsonConverter +{ + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => ReadNumber(reader), + JsonTokenType.Null => null, + _ => throw new JsonException($"Unexpected token {reader.TokenType} for string-or-number value.") + }; + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStringValue(value); + } + + private static string ReadNumber(Utf8JsonReader reader) + { + if (reader.TryGetInt64(out var longValue)) + { + return longValue.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + if (reader.TryGetDouble(out var doubleValue)) + { + return doubleValue.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + return reader.GetDecimal().ToString(System.Globalization.CultureInfo.InvariantCulture); + } +} diff --git a/v2rayN/ServiceLib/Models/Entities/RulesItem.cs b/v2rayN/ServiceLib/Models/Entities/RulesItem.cs index 5c7c07d8..63349df7 100644 --- a/v2rayN/ServiceLib/Models/Entities/RulesItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/RulesItem.cs @@ -5,6 +5,7 @@ public class RulesItem { public string Id { get; set; } public string? Type { get; set; } + [JsonConverter(typeof(JsonStringOrNumberConverter))] public string? Port { get; set; } public string? Network { get; set; } public List? InboundTag { get; set; }