Ruby中的奇怪编码问题:ASCII!= UTF-8,但UTF-8 == ASCII

3
下面代码返回的值是"\x88\x90r\"\x9EN\xFFR":
MyApp::XVP::xvp_password_encrypt_vnc("L1UkDr]c")
# => "\x88\x90r\"\x9EN\xFFR"

当我们在测试中使用这个时:
should "correctly encrypt a vnc password" do
  assert MyApp::XVP::xvp_password_encrypt_vnc("L1UkDr]c") == "\x88\x90r\"\x9EN\xFFR"
end
# => false

这是一个编码问题,我们可以通过以下方式看到:
MyApp::XVP::xvp_password_encrypt_vnc("L1UkDr]c").encoding
# => #<Encoding:ASCII-8BIT>

"\x88\x90r\"\x9EN\xFFR".encoding
# => #<Encoding:UTF-8>

因此,比较失败是有道理的,修复方法是在 xvp_password_encrypt_vnc 方法末尾强制转换为 UTF 编码,如下所示:

def xvp_password_encrypt_vnc(hex)
  des = OpenSSL::Cipher::Cipher.new("des-ecb")
  ... etc 
  des.update(hex).force_encoding('UTF-8')
end

现在,我们失败的测试通过了:
should "correctly encrypt a vnc password" do
  assert MyApp::XVP::xvp_password_encrypt_vnc("L1UkDr]c").force_encoding("UTF-8") == "\x88\x90r\"\x9EN\xFFR"
end
# => true

但是反过来似乎不起作用:

# This should fail
should "correctly encrypt a vnc password" do
  MyApp::XVP::xvp_password_decrypt_vnc("\x88\x90r\"\x9EN\xFFR") == "L1UkDr]c"
end
# => true

以上方法应该失败的原因是因为我们再次比较ASCII-8bit和UTF-8(之前已经失败了):
MyApp::XVP::xvp_password_decrypt_vnc("\x88\x90r\"\x9EN\xFFR").encoding
# => #<Encoding:ASCII-8BIT>

"L1UkDr]c".encoding
# => #<Encoding:UTF-8>

为什么只能单向传输:

something encoded in ASCII 8-bit != same thing encoded in UTF-8

但是当我们从另一个方向进行时,它不会失败:
something encoding in UTF-8 == same thing encoded in ASCII 8-bit

1
"\x88\x90r\"\x9EN\xFFR".valid_encoding? 可以帮助回答你的问题。你的问题在于该字符串不是“以UTF-8编码的相同内容”,这些字节甚至不是有效的UTF-8。然而,纯文本密码的ASCII和UTF-8编码作为字节和字符是等效的。 - Neil Slater
1
你有什么问题? - sawa
1
@sawa,请阅读最后一部分,我问道:“那么为什么它在单向传输时失败了.....但是在另一个方向上却没有失败”。 - stephenmurdoch
@NeilSlater 谢谢你的解释,这解释了很多问题。 - stephenmurdoch
2个回答

3
记住,编码是为人机交互设计的,而密码是为计算机 - 计算机交互设计的。当构建密码时,您实际上创建了一个没有固有编码的比特流。
为了补偿 Ruby 解释带有编码的字符串的倾向,您可以将值转换为 Base64,如下所示:
require 'base64'

module MyApp::XVP
  def xvp_password_encrypt_vnc64(hex)
    Base64.strict_encode64 xvp_password_encrypt_vnc(hex)
  end

  def xvp_password_decrypt_vnc64(hex)
    xvp_password_decrypt_vnc Base64.strict_decode64(hex)
  end
end

你可以使用这些方法的输出进行测试。

另一种可能性是将规范数据转换为Encoding::BINARY(它是Encoding::ASCII_8BIT的别名):

context 'decoding password'
  let(:encoded) { "\x88\x90r\"\x9EN\xFFR".force_encoding('BINARY') }
  let(:decoded) { "L1UkDr]c" }

  subject { MyApp::XVP::xvp_password_decrypt_vnc(encoded) }
  it { should eq decoded }
end

谢谢DMKE,这给了我一个很好的想法,知道我应该做什么。 - stephenmurdoch
很好的解释.. +1 - Arup Rakshit

0
两种情况的区别不在于你使用哪种“方式”进行比较,而是被比较的字符串的性质。文档对此并不清楚,但当比较两个字符串且它们具有不同的编码时,Ruby会检查它们是否可比较。
特别地,如果一个字符串具有ASCII-8BIT编码,并且仅由小于x80(即仅在ASCII范围内)的字节组成,则可以将其与具有UTF-8等ASCII兼容编码的字符串进行比较。如果它包含ASCII范围之外的字节(大于x7f),则无法将其与另一种编码的字符串进行比较。
在第一种情况下,字符串为"\x88\x90r\"\x9EN\xFFR",其中包含非ASCII字节,因此与标记为UTF-8的字符串进行比较时,即使UTF-8字符串实际上包含相同的字节(请注意,在这种情况下,这不是有效的UTF-8字符串),也会被视为不相等。换句话说,以下两种比较都返回false:
u = "\x88\x90r\"\x9EN\xFFR" # default utf-8 encoding
b = "\x88\x90r\"\x9EN\xFFR".force_encoding('ASCII-8BIT') 

# utf-8 == ascii 8bit
puts u == b

# ascii 8bit == utf-8
puts b == u

第二个字符串是"L1UkDr]c",它仅由ASCII范围内的字节(小于0x80)组成,因此可以与UTF-8字符串进行比较。这段代码对于两种情况都会产生true
u = "L1UkDr]c" # default utf-8 encoding
b = "L1UkDr]c".force_encoding('ASCII-8BIT') 

# utf-8 == ascii 8bit
puts u == b

# ascii 8bit == utf-8
puts b == u

当组合不同编码的字符串时,使用相同(或至少类似)的规则。例如,在第一种情况下(字符串中有非ASCII字节),尝试执行u + b将导致Encoding :: CompatibilityError ,而在第二种情况下,您只会得到字符串"L1UkDr]cL1UkDr]c"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接