URI.unescape在尝试将"%C3%9Fą"转换为"ßą"时崩溃

Bul*_*tor 2 ruby crash encoding uri character-encoding

我正在使用 URI.unescape来转换文本,不幸的是我遇到了奇怪的错误:

 # encoding: utf-8
 require('uri')
 URI.unescape("%C3%9F?")
Run Code Online (Sandbox Code Playgroud)

结果是

 C:/Ruby193/lib/ruby/1.9.1/uri/common.rb:331:in `gsub': incompatible character encodings: ASCII-8BIT and UTF-8 (Encoding::CompatibilityError)
    from C:/Ruby193/lib/ruby/1.9.1/uri/common.rb:331:in `unescape'
    from C:/Ruby193/lib/ruby/1.9.1/uri/common.rb:649:in `unescape'
    from exe/fail.rb:3:in `<main>'
Run Code Online (Sandbox Code Playgroud)

为什么?

Vas*_*ich 9

不知道为什么,但你可以使用CGI.unescape方法:

# encoding: utf-8
require 'cgi'
CGI.unescape("%C3%9F?")
Run Code Online (Sandbox Code Playgroud)


mu *_*ort 5

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
Run Code Online (Sandbox Code Playgroud)

正在使用的正则表达式是/%[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
Run Code Online (Sandbox Code Playgroud)

ASCII-8BIT编码也称为"二进制"(即没有特定编码的普通字节).然后块返回字节和String#gsub试图插入的UTF-8编码的副本strgsub正在研究,你会得到你的错误:

不兼容的字符编码: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
Run Code Online (Sandbox Code Playgroud)

一旦开始将非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
Run Code Online (Sandbox Code Playgroud)

另一个选择是推force_encoding入块:

def unescape(str, escaped = @regexp[:ESCAPED])
  str.gsub(escaped) { [$&[1, 2].hex].pack('C').force_encoding(encoding) }
end
Run Code Online (Sandbox Code Playgroud)

我不确定为什么gsub在某些情况下失败但在其他情况下成功.