Uri.TryUnescapeDataString 缓冲区不足,却抛出 ArgumentOutOfRangeException
目标 Span 短于百分号之前的普通前缀时,.NET 10.0.12 会先越界。按输入长度准备空间,或补上前缀容量检查,保留 false 契约。
调用 Uri.TryUnescapeDataString 时,目标 Span 太短,本来准备处理 false,程序却先抛了 ArgumentOutOfRangeException。若输入在第一个百分号前还有普通文本,应检查这一段前缀是否已经长于目标缓冲区。
.NET 10.0.12 的实现仍存在这条越界路径。API 文档规定,目标放不下结果时应返回 false;这里不是“Try 方法通常也可能抛异常”就能解释过去的正常容量检查行为。
最小触发形态如下:
using System;
Uri.TryUnescapeDataString("Prefix %20".AsSpan(), Span<char>.Empty, out _);
第一个 % 前有七个字符,目标长度却为零。实现尚未判断空间是否足够,就先按前缀长度切目标 Span,因而抛异常。把 %20 移到开头会改变这条路径,所以“另一个输入返回 false”不能排除问题。
大小可控时,按输入长度准备空间
百分号反转义不会让字符数超过输入长度。对于已经通过应用长度限制的小型输入,可以准备同等长度的独立数组,调用后只使用实际写入部分:
using System;
static string DecodeComponent(string input)
{
ArgumentNullException.ThrowIfNull(input);
char[] buffer = new char[input.Length];
if (!Uri.TryUnescapeDataString(input.AsSpan(), buffer, out int written))
{
throw new InvalidOperationException("The decode buffer was too small.");
}
return new string(buffer, 0, written);
}
调用前仍由业务入口限制输入大小,不能让不受限外部字符串直接决定内存分配。上例不使用大尺寸 stackalloc,也没有池化数组归还和清理问题;若项目采用 ArrayPool,应保留原有租用、异常归还及敏感内容清理规则。
这里的长度理由针对解码后的字符,不是说任意输入都合法。这个 API 不负责证明整条 URI 符合业务约束,也不等同于表单解码;不能据此把 + 当成空格,或在后续路径中再次反转义。
必须保留调用方小缓冲区时,先挡住前缀切片
若调用契约就是“容量不足时返回 false”,可在受影响运行时外层补上缺失的前缀检查:
using System;
static bool TryDecodeComponent(
ReadOnlySpan<char> input, Span<char> destination, out int written)
{
int firstPercent = input.IndexOf('%');
if (firstPercent >= 0 && destination.Length < firstPercent)
{
written = 0;
return false;
}
return Uri.TryUnescapeDataString(input, destination, out written);
}
这只补本题已经定位的容量分支,不承诺屏蔽 API 的所有其他异常。其他输入检查、内存重叠约束和错误处理继续遵守原接口。返回 false 后不要读取 destination 中的残留内容,只有成功结果的 written 区间才应交给下游。
正式升级也要看实际发布版本的实现。主分支已经在切片前检查前缀长度,但不能把合入记录直接当成 .NET 9、10 的回补证明;至少在 10.0.12 的正式源码中,这个检查仍缺失。若目标运行时有新的维护版本,用其正式源码和同一输入确认,不为了绕过这一个分支直接切换整套运行时大版本。
复查时覆盖百分号在开头、在普通前缀之后、没有百分号、空输入,以及恰好够用和不足的目标空间。原异常应消失,容量不足按约定返回 false,足够空间的结果与预期字符一致。保留新增包装函数及调用点的版本差异;将来移除临时检查前,同样复核部署机器的实际运行时,不能只看编译所用 SDK。


