😊 新增 流变对象支持 application/x-www-form-urlencoded 表单数据进行转换

This commit is contained in:
MonkSoul
2025-11-25 14:49:29 +08:00
parent c40570b72b
commit 91a8859a1f
10 changed files with 250 additions and 66 deletions

View File

@@ -26,8 +26,10 @@
using Microsoft.Extensions.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
namespace Furion.Extensions;
@@ -354,6 +356,120 @@ internal static partial class StringExtensions
internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
/// <summary>
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
/// </summary>
/// <param name="output">字符串</param>
/// <returns>
/// <see cref="bool" />
/// </returns>
internal static bool IsUrlEncodedFormFormat(this string output)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(output);
return UrlEncodedFormFormatRegex().IsMatch(output);
}
/// <summary>
/// 将 <c>application/x-www-form-urlencoded</c> 格式的字符串解析为 <see cref="JsonObject" />
/// </summary>
/// <param name="formData">URL 编码的表单数据字符串</param>
/// <returns>
/// <see cref="JsonObject" />
/// </returns>
internal static JsonObject? ParseUrlEncodedFormToJsonObject(this string? formData)
{
// 尝试移除开头的 ?
formData = formData?.TrimStart('?');
// 空检查
if (string.IsNullOrWhiteSpace(formData))
{
return null;
}
// 初始化 JsonObject 实例
var root = new JsonObject();
// 按 & 分割每个键值对
foreach (var part in formData.Split('&'))
{
// 查找第一个 =
var eqIndex = part.IndexOf('=');
// 键名为空或不存在 = 则跳过
if (eqIndex <= 0)
{
continue;
}
// URL 解码键和值
var key = WebUtility.UrlDecode(part[..eqIndex]);
var value = WebUtility.UrlDecode(part[(eqIndex + 1)..]);
// 将键名(如 user[0][name])拆分为 token["user", "0", "name"]
var tokens = key.Replace("]", "").Split('[');
var current = root;
var i = 0;
// 逐层构建嵌套结构
while (i < tokens.Length)
{
// 空检查
var token = tokens[i];
if (string.IsNullOrEmpty(token))
{
i++;
continue;
}
// 检查是否是最后一项
if (i == tokens.Length - 1)
{
current[token] = value;
break;
}
// 下一项为数组索引,按数组处理
var nextToken = tokens[i + 1];
if (int.TryParse(nextToken, out var index) && index >= 0)
{
// 确保当前 token 是一个 JsonArray
if (current[token] is not JsonArray array)
{
array = [];
current[token] = array;
}
// 扩容数组
while (array.Count <= index)
{
array.Add(new JsonObject());
}
// 进入该数组元素并跳过数组名和索引
current = (JsonObject)array[index]!;
i += 2;
}
else
{
// 下一个 token 是普通属性名则视为对象名
if (current[token] is not JsonObject child)
{
child = new JsonObject();
current[token] = child;
}
current = child;
i++;
}
}
}
return root;
}
/// <summary>
/// 占位符匹配正则表达式
/// </summary>
@@ -376,4 +492,9 @@ internal static partial class StringExtensions
/// </returns>
[GeneratedRegex(@"\[\[\s*([\w\-:]+)((?:\s*\|\s*[\w\-:]+)*)\s*(?:\|\|\s*([^\]]*))?\s*\]\]")]
private static partial Regex ConfigurationKeyRegex();
[GeneratedRegex(
"^(?:(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)(?:&(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)*$",
RegexOptions.IgnorePatternWhitespace)]
private static partial Regex UrlEncodedFormFormatRegex();
}

View File

@@ -30,7 +30,6 @@ using System.Net;
using System.Net.Http.Json;
using System.Net.Mime;
using System.Text;
using System.Text.RegularExpressions;
using ContentDispositionHeaderValue = System.Net.Http.Headers.ContentDispositionHeaderValue;
namespace Furion.HttpRemote;
@@ -155,21 +154,6 @@ internal static partial class Helpers
return HttpMethod.Parse(httpMethod);
}
/// <summary>
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
/// </summary>
/// <param name="output">字符串</param>
/// <returns>
/// <see cref="bool" />
/// </returns>
internal static bool IsFormUrlEncodedFormat(string output)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(output);
return FormUrlEncodedFormatRegex().IsMatch(output);
}
/// <summary>
/// 检查 HTTP 状态码是否是重定向状态码并返回重定向时应使用的 HTTP 请求方法
/// </summary>
@@ -298,14 +282,4 @@ internal static partial class Helpers
return null;
}
/// <summary>
/// <c>application/x-www-form-urlencoded</c> 格式正则表达式
/// </summary>
/// <returns>
/// <see cref="Regex" />
/// </returns>
[GeneratedRegex(
@"^(?:(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)(?:&(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)*$")]
private static partial Regex FormUrlEncodedFormatRegex();
}

View File

@@ -24,7 +24,6 @@
// ------------------------------------------------------------------------
using Furion.Extensions;
using System.Globalization;
using System.Net.Http.Headers;
using System.Text;
@@ -49,7 +48,7 @@ public class StringContentForFormUrlEncodedContentProcessor : FormUrlEncodedCont
}
// 如果原始内容是字符串类型且不是有效的 application/x-www-form-urlencoded 格式
if (rawContent is string rawString && !Helpers.IsFormUrlEncodedFormat(rawString))
if (rawContent is string rawString && !rawString.IsUrlEncodedFormFormat())
{
throw new FormatException("The content must contain only form url encoded string.");
}

View File

@@ -23,7 +23,6 @@
// 请访问 https://gitee.com/dotnetchina/Furion 获取更多关于 Furion 项目的许可证和版权信息。
// ------------------------------------------------------------------------
using Furion.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
@@ -31,7 +30,6 @@ using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Web;
namespace Furion.Shapeless;
@@ -89,9 +87,7 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
return string.IsNullOrEmpty(json)
? (false, null)
: (true,
Clay.Parse(isFormUrlEncoded ? HttpUtility.UrlDecode(json).ParseFormatKeyValueString(['&'], '?') : json,
options));
: (true, Clay.Parse(json, options));
}
/// <summary>

View File

@@ -255,6 +255,8 @@ public partial class Clay
// 将对象转换为 JsonNode 实例
var jsonNode = obj switch
{
// 处理 application/x-www-form-urlencoded 格式数据
string formData when formData.IsUrlEncodedFormFormat() => formData.ParseUrlEncodedFormToJsonObject(),
string rawJson => JsonNode.Parse(rawJson, jsonNodeOptions, jsonDocumentOptions),
Stream utf8Json => JsonNode.Parse(utf8Json, jsonNodeOptions, jsonDocumentOptions),
byte[] utf8JsonBytes => JsonNode.Parse(utf8JsonBytes, jsonNodeOptions, jsonDocumentOptions),

View File

@@ -26,8 +26,10 @@
using Microsoft.Extensions.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
namespace Furion.Extensions;
@@ -354,6 +356,120 @@ internal static partial class StringExtensions
internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
/// <summary>
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
/// </summary>
/// <param name="output">字符串</param>
/// <returns>
/// <see cref="bool" />
/// </returns>
internal static bool IsUrlEncodedFormFormat(this string output)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(output);
return UrlEncodedFormFormatRegex().IsMatch(output);
}
/// <summary>
/// 将 <c>application/x-www-form-urlencoded</c> 格式的字符串解析为 <see cref="JsonObject" />
/// </summary>
/// <param name="formData">URL 编码的表单数据字符串</param>
/// <returns>
/// <see cref="JsonObject" />
/// </returns>
internal static JsonObject? ParseUrlEncodedFormToJsonObject(this string? formData)
{
// 尝试移除开头的 ?
formData = formData?.TrimStart('?');
// 空检查
if (string.IsNullOrWhiteSpace(formData))
{
return null;
}
// 初始化 JsonObject 实例
var root = new JsonObject();
// 按 & 分割每个键值对
foreach (var part in formData.Split('&'))
{
// 查找第一个 =
var eqIndex = part.IndexOf('=');
// 键名为空或不存在 = 则跳过
if (eqIndex <= 0)
{
continue;
}
// URL 解码键和值
var key = WebUtility.UrlDecode(part[..eqIndex]);
var value = WebUtility.UrlDecode(part[(eqIndex + 1)..]);
// 将键名(如 user[0][name])拆分为 token["user", "0", "name"]
var tokens = key.Replace("]", "").Split('[');
var current = root;
var i = 0;
// 逐层构建嵌套结构
while (i < tokens.Length)
{
// 空检查
var token = tokens[i];
if (string.IsNullOrEmpty(token))
{
i++;
continue;
}
// 检查是否是最后一项
if (i == tokens.Length - 1)
{
current[token] = value;
break;
}
// 下一项为数组索引,按数组处理
var nextToken = tokens[i + 1];
if (int.TryParse(nextToken, out var index) && index >= 0)
{
// 确保当前 token 是一个 JsonArray
if (current[token] is not JsonArray array)
{
array = [];
current[token] = array;
}
// 扩容数组
while (array.Count <= index)
{
array.Add(new JsonObject());
}
// 进入该数组元素并跳过数组名和索引
current = (JsonObject)array[index]!;
i += 2;
}
else
{
// 下一个 token 是普通属性名则视为对象名
if (current[token] is not JsonObject child)
{
child = new JsonObject();
current[token] = child;
}
current = child;
i++;
}
}
}
return root;
}
/// <summary>
/// 占位符匹配正则表达式
/// </summary>
@@ -376,4 +492,9 @@ internal static partial class StringExtensions
/// </returns>
[GeneratedRegex(@"\[\[\s*([\w\-:]+)((?:\s*\|\s*[\w\-:]+)*)\s*(?:\|\|\s*([^\]]*))?\s*\]\]")]
private static partial Regex ConfigurationKeyRegex();
[GeneratedRegex(
"^(?:(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)(?:&(?:[a-zA-Z0-9-._~]|%[0-9A-Fa-f]{2})+=(?:[a-zA-Z0-9-._~+]|%[0-9A-Fa-f]{2})*)*$",
RegexOptions.IgnorePatternWhitespace)]
private static partial Regex UrlEncodedFormFormatRegex();
}

View File

@@ -30,7 +30,6 @@ using System.Net;
using System.Net.Http.Json;
using System.Net.Mime;
using System.Text;
using System.Text.RegularExpressions;
using ContentDispositionHeaderValue = System.Net.Http.Headers.ContentDispositionHeaderValue;
namespace Furion.HttpRemote;
@@ -155,21 +154,6 @@ internal static partial class Helpers
return HttpMethod.Parse(httpMethod);
}
/// <summary>
/// 验证字符串是否是 <c>application/x-www-form-urlencoded</c> 格式
/// </summary>
/// <param name="output">字符串</param>
/// <returns>
/// <see cref="bool" />
/// </returns>
internal static bool IsFormUrlEncodedFormat(string output)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(output);
return FormUrlEncodedFormatRegex().IsMatch(output);
}
/// <summary>
/// 检查 HTTP 状态码是否是重定向状态码并返回重定向时应使用的 HTTP 请求方法
/// </summary>
@@ -298,14 +282,4 @@ internal static partial class Helpers
return null;
}
/// <summary>
/// <c>application/x-www-form-urlencoded</c> 格式正则表达式
/// </summary>
/// <returns>
/// <see cref="Regex" />
/// </returns>
[GeneratedRegex(
@"^(?:(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)(?:&(?:[a-zA-Z0-9-._~]+|%(?:[0-9A-Fa-f]{2}))+=(?:[a-zA-Z0-9-._~]*|%(?:[0-9A-Fa-f]{2}))+)*$")]
private static partial Regex FormUrlEncodedFormatRegex();
}

View File

@@ -24,7 +24,6 @@
// ------------------------------------------------------------------------
using Furion.Extensions;
using System.Globalization;
using System.Net.Http.Headers;
using System.Text;
@@ -49,7 +48,7 @@ public class StringContentForFormUrlEncodedContentProcessor : FormUrlEncodedCont
}
// 如果原始内容是字符串类型且不是有效的 application/x-www-form-urlencoded 格式
if (rawContent is string rawString && !Helpers.IsFormUrlEncodedFormat(rawString))
if (rawContent is string rawString && !rawString.IsUrlEncodedFormFormat())
{
throw new FormatException("The content must contain only form url encoded string.");
}

View File

@@ -23,7 +23,6 @@
// 请访问 https://gitee.com/dotnetchina/Furion 获取更多关于 Furion 项目的许可证和版权信息。
// ------------------------------------------------------------------------
using Furion.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
@@ -31,7 +30,6 @@ using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Web;
namespace Furion.Shapeless;
@@ -89,9 +87,7 @@ internal sealed class ClayBinder(IOptions<ClayOptions> options) : IModelBinder
return string.IsNullOrEmpty(json)
? (false, null)
: (true,
Clay.Parse(isFormUrlEncoded ? HttpUtility.UrlDecode(json).ParseFormatKeyValueString(['&'], '?') : json,
options));
: (true, Clay.Parse(json, options));
}
/// <summary>

View File

@@ -255,6 +255,8 @@ public partial class Clay
// 将对象转换为 JsonNode 实例
var jsonNode = obj switch
{
// 处理 application/x-www-form-urlencoded 格式数据
string formData when formData.IsUrlEncodedFormFormat() => formData.ParseUrlEncodedFormToJsonObject(),
string rawJson => JsonNode.Parse(rawJson, jsonNodeOptions, jsonDocumentOptions),
Stream utf8Json => JsonNode.Parse(utf8Json, jsonNodeOptions, jsonDocumentOptions),
byte[] utf8JsonBytes => JsonNode.Parse(utf8JsonBytes, jsonNodeOptions, jsonDocumentOptions),