URI.unescape
非 ASCII 输入的实现被破坏。1.9.3 版本如下所示:
def unescape(str, escaped = @regexp[:ESCAPED])
str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(str.encoding)
end
使用的正则表达式是/%[a-fA-F\d]{2}/
. 所以它通过字符串寻找一个百分号后跟两个十六进制数字;块$&
中将是匹配的文本(例如'%C3')并且$&[1,2]
是没有前导百分号('C3'
)的匹配文本。然后我们调用String#hex
将该十六进制数转换为 Fixnum ( 195
) 并将其包装在 Array ( [195]
) 中,以便我们可以使用它Array#pack
来为我们进行字节修饰。问题是这pack
给了我们一个二进制字节:
> puts [195].pack('C').encoding
ASCII-8BIT
ASCII-8BIT 编码也称为“二进制”(即没有特定编码的纯字节)。然后该块返回该字节并String#gsub
尝试插入正在处理的 UTF-8 编码副本中str
,gsub
您会得到错误:
不兼容的字符编码:ASCII-8BIT 和 UTF-8 (Encoding::CompatibilityError)
因为您不能(通常)将二进制字节填充到 UTF-8 字符串中;你经常可以侥幸逃脱:
URI.unescape("%C3%9F") # Works
URI.unescape("%C3µ") # Fails
URI.unescape("µ") # Works, but nothing to gsub here
URI.unescape("%C3%9Fµ") # Fails
URI.unescape("%C3%9Fpancakes") # Works
一旦您开始将非 ASCII 数据混合到您的 URL 编码字符串中,事情就会开始分崩离析。
一个简单的解决方法是在尝试解码之前将字符串切换为二进制:
def unescape(str, escaped = @regexp[:ESCAPED])
encoding = str.encoding
str = str.dup.force_encoding('binary')
str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding)
end
另一种选择是将 推force_encoding
入块中:
def unescape(str, escaped = @regexp[:ESCAPED])
str.gsub(escaped) { [$&[1, 2].hex].pack('C').force_encoding(encoding) }
end
我不确定为什么gsub
在某些情况下失败但在其他情况下成功。