为什么 (number).toString(32) 的结果与其他 Base32 编码器实现不同?

时间:2021-07-04 11:10:04

标签: javascript encoding bit-manipulation bitwise-operators base32

背景

我想使用 Douglas Crockford 的 Base32 实现对我在 Web 应用程序中创建的随机整数进行编码,在以下 URL https://www.crockford.com/base32.html 中进行了描述。我曾计划自己构建编码器作为学习练习,但“较低级别”的细节对我来说有点像潘多拉魔盒。

问题

  1. 使用我尝试过的 Base32 实现(例如 https://github.com/agnoster/base32-js)对 "12345" 进行编码会产生 "64t36d1n"
  2. 使用 (12345).toString(32) 编码相同的数字会得到 c1p,这对我来说更有意义,因为它更短(这就是我的目标)。

我的直觉是,区别在于操作字符串而不是数字。然而,检查我尝试过的实现代码发现它们无论如何都使用类似于 byte.charCodeAt(0) 的东西将字符串转换为整数,所以天真告诉我这是一样的。

我会使用 2.,除了我想控制字母表的事实(例如省略 U、I 等)。如果知道的人可以帮助我指出正确的方向并帮助我提高对这个主题的理解,我将不胜感激。非常感谢。

1 个答案:

答案 0 :(得分:2)

这种混淆可能源于这样一个事实,即当您说“base 32”时,您可能指的是两种不同的事物(尽管密切相关)。

事情 1:数字基数

数字表示的结构方式,定义了单个“数字”具有多少个不同的符号选项。我们人类通常使用基数 10 来表示我们的数字有 10 个不同的符号 (0-9),但您也可以使用以 2 为基数的二进制(仅使用符号 0 和 1)、以 8 为基数的八进制(使用符号 0-7 ) 等等。见base/radix on Wikipedia。对于大于 10 的基数,您通常使用字母,从第 11 个符号的 A 开始。例如,十六进制(基数为 16)使用符号 0-9 和 A-F。

由于除了 0-9 的 10 个符号之外,我们只有 26 个不同的字母可以使用,因此在大多数情况下,只定义了以 36 为基数的表示。如果您尝试运行 12345..toString(40),您将因此获得 RangeError: toString() radix argument must be between 2 and 36

现在,以这种方式(使用符号 0-9 和 AV)表示以 32 为基数的数字 12345 将为您提供 C1P,因为 C 的值为 13,1 的值为1 和 P 的值为 25,而 13 * 32^2 + 1 * 32^1 + 25 * 32^0 等于 12345。这只是一种使用 32 个不同符号而不是 10 来写数字的方法,我不会称之为“编码”。

如果基数大于 10,这将导致比常规基数 10 更短1(或同样长)的表示。

事情 2:基本N 编码

base64”(最著名的此类编码)中的“baseN”编码一词表示八位字节流(字节流, 8 位二进制2 数据)使用“字母表”(一组允许的符号),其中 N 指定字母表有多少个符号3< /sup>。这用于在不允许使用字节中所有可能值的介质上存储或传输任何八位字节流(无论其内容如何)(例如,诸如电子邮件之类的介质,它可能只包含文本,或不允许使用某些特殊字符(例如 /?)的 URL,因为它们具有语义意义等 - 甚至是一张纸,因为符号 {{1} } 和 0 以及 OIl 不能可靠地使用而不会在阅读时混淆它们的危险一个人)。

现在是标记与第一个“事物”关系的部分:可以通过将输入字节转换为一个巨大的数字并更改其基数来想象转换的工作方式,但使用编码定义的字母表,不一定数字后跟字母。可以在 here 中找到一个很好的视觉解释。

“将输入字节转换为一个巨大的数字”部分是您提到的 1 发挥作用的地方:例如,我可以将字符串 charCodeAt 转换为数字 4276803,它变得更多在查看以十六进制表示的字节时很明显,因为一个字节可以有 256 个值,这个范围正好适合两个十六进制“数字”的确切范围(0x00-0xFF4)。 ABC 中的三个字节5 的十六进制值分别为 0x65、0x66 和 0x67,如果将它们并排放置,我可以将它们视为一个大数 0x656667 = 4276803。

与第一个“事情”的另一个重叠是,在密码学中,非常大的数字会发挥作用,而且通常这些数字也使用 base32 或 base58 或 base64 之类的机制进行编码,但除非编程语言和/或处理器具有适合大数的数据类型/寄存器,此时该数已再次表示为某种八位字节流(与我刚刚描述的相反,但有时使用相反的字节顺序)。

当然这只是概念上它是如何完成的,因为否则一旦我们谈论编码不是 3 字节而是 3,000,000 字节,算法将不得不处理巨大的数字。实际上,使用涉及位移等的巧妙方法可在任何长度的数据上顺序实现相同的结果。

因为您习惯看到的“字符串”(暂时忽略 Unicode)可以与以 256 为基数表示的字节的数字进行粗略的比较(一个字节中可能的 256 个值中的每一个都有一个符号) ),这意味着任何这样的 baseN 编码将使输出更长,因为新的“基数”低于 256。请注意,将 ABC 放入base32 算法将表示 string 12345,在我上面的解释中,它可以被视为数字 211295614005(或 0x3132333435)。从这个角度来看,12345(你从 base32 得到的)肯定比以 10 为底的 211295614005 短,所以这一切又有意义了。

重要说明:如果您的输入数据由于其长度而无法在没有填充的情况下准确映射到其新表示,则此解释并不完全正确。例如,一个 3 字节长的数据块占用 3*8=24 位,并且每个符号使用 6 位的 base64 表示很容易实现,因为这些符号中正好有四个也将占用 4*6=24 位。但是一个 4 字节长的数据块占用 4*8=32 位,因此需要 5.333...base64 中的符号(5.333...*6=32)。为了“填满”剩余的数据空间,使用了某种填充6,因此我们可以将其四舍五入为 6 个符号。通常,填充被添加到输入数据的末尾,这就是现实与我上面“改变大数基数”概念的不同之处,因为在数学中你会期望领先< /em> 零作为填充。


道格拉斯·克罗克福德的 base32

解决您最初的问题:

Douglas Crockford 的 base32 算法实际上是为数字而设计的,但使用经过修改的字母表,它不像程序员习惯的那样将八位字节流作为输入。所以它更像是上述两件事的中间立场。您是对的,64t36d1n 可以满足您的需要,但是您必须在基数 32(0-9,AV,不区分大小写)和 Crockford's(0- 9 和 AZ 但没有 I、O 和 U,不区分大小写,解码时将 I 映射到 1,将 O 映射到 0)。

来回替换这些东西已经足够复杂了,我想自己从头开始编写算法而不是依赖 toString(32) 会更清晰(也更有教育意义)。

(另外,Crockford 在结尾处提出了一个额外的“检查符号”,无论如何都超出了这里的解释。)


脚注:

1: 这是假设的整数。如果您有分数,那么情况就大不相同了,因为对于旧基数中没有重复小数的数字,您可以在新基数中得到重复小数,或者反过来。例如,以 32 为底的 0.1 是 0.36CPJ6CPJ6CPJ6CPJ...这是一个无限长的数字(在该特定表示中)。

2: 这里的术语“二进制”不是指基数 2 中的表示,而是指“任何类型的数据,它可以使用从 0 到 255 每个字节,不限于表示 ASCII 范围 32-126" 中人类可读文本的值。

3:请注意,仅从 N 来看,您无法推断出字母表的确切含义,只能推断出它的长度。众所周知的编码具有普遍接受的关于使用哪个字母表的约定,例如 base64 和 base58(后者通常用于加密货币地址,顺便说一下,它的字母表甚至不是按字母顺序排列的)。请注意,即使对于 base64 也有类似 base64url 的变体,它们会略微改变字母表。其他如 base32 还没有普遍接受的字母表,这就是为什么您链接的网站提到“这是 a base32 编码而不是 base32 编码” - 特别是它与克罗克福德的字母表不一样。

4:前缀 toString 通常用于表示以下符号将被解释为以 16 为基数(十六进制)而不是以 10 为基数的数字。< /sub>

5:我在这里谈论的是字节,因为这是基本N算法使用的,但实际上字符串基于characters 而不是字节,它们也可能包含数值超过 255 的 Unicode 字符,因此不再适合单个字节。通常,字符串首先使用字符编码(如 UTF-8 到字节)进行编码,然后对这些字节执行 baseN 编码。

6: base64 使用 0x 作为填充,并且为了保留使用了多少填充字符的信息,还附加了相同数量的 = 字符到输出(= 不在 base64 的字母表中)。