Base64长度计算?

223

阅读base64 维基 后...

我正试图弄清楚公式的作用:

给定长度为n的字符串,base64的长度将为enter image description here

即: 4*Math.Ceiling(((double)s.Length/3)))

我已经知道base64的长度必须为%4==0,以便解码器知道原始文本长度。

序列的最大填充数可以是===

维基:每个输入字节的输出字节数约为4/3(33%开销)

问题:

如何将上述信息与输出长度enter image description here结合起来?

16个回答

298

每个字符用于表示6位(log2(64) = 6)。

因此,使用4个字符来表示4 * 6 = 24位 = 3字节

因此,您需要4*(n/3)个字符来表示n个字节,并且需要将其向上舍入为4的倍数。

向上舍入为4的倍数后导致未使用填充字符的数量显然为0、1、2或3个。


2
这里的填充在哪里? - Royi Namir
3
如果有一个字节的输入,那么将产生四个字符的输出。但是只需要两个输出字符来编码输入。因此会有两个字符作为填充。 - David Schwartz
4
输出长度总是向上取整到4的倍数,因此1、2或3个输入字节 => 4个字符;4、5或6个输入字节 => 8个字符;7、8或9个输入字节 => 12个字符。 - Paul R
6
我在上面的答案中已经解释了这些内容:(i) 每个输出的字符代表6个输入的比特,(ii) 因此4个输出的字符代表4 * 6 = 24个比特,(iii) 24个比特等于3个字节,(iv) 因此3个字节的输入会产生4个字符的输出,(v) 输出的字符与输入的字节之比为4 / 3。 - Paul R
2
@techie_28:我把它做成了27308个字符,对应20 * 1024字节,但今天早上我还没有喝咖啡。 - Paul R
显示剩余6条评论

89

4 * n / 3 会得到未填充长度。

然后将其四舍五入到最接近的4的倍数,由于4是2的幂,因此可以使用位逻辑操作。

((4 * n / 3) + 3) & ~3

1
你是正确的!4 * n / 3 给出未填充长度!上面的答案是不正确的。((4 * n / 3) + 3) & ~3 返回正确的结果。 - Cadburry
3
用shell语言解释一下:$(( ((4 * n / 3) + 3) & ~3 )),意为:将n乘以4/3再加上3,然后向下取整至最接近的能被4整除的数。 - starfry
3
"4 * n / 3" 在 n=1 时已经失败了,一个字节被编码成两个字符,而结果显然只有一个字符。" - Maarten Bodewes
1
我认为这里需要考虑每76个字符的 '\n',因为我已经看到一些base64实现规范要求这样做。你提醒了填充的必要性,我一直在想为什么我的实际值和期望值不匹配。 - StoneThrow
1
@Crog 正如所述,如果 n = 1,则使用整数得到 4/3 = 1。正如您所指出的,期望结果为 2,而不是 1。 - Maarten Bodewes
显示剩余2条评论

39

参考资料,Base64编码器的长度公式如下:

Base64编码器的长度公式

正如你所说,给定n个字节的数据,一个Base64编码器将产生一个由4n / 3个Base64字符组成的字符串。换句话说,每3个字节的数据将产生4个Base64字符。修改:一条评论正确指出我以前的图形没有考虑到填充;填充的正确公式为4(向上取整(n/3))

维基百科文章中举例说明了ASCII字符串Man是如何编码为Base64字符串TWFu。输入字符串大小为3个字节或24位,因此该公式正确预测输出长度将为4个字节(或32位): TWFu。该过程将6个数据位编码成64个Base64字符之一,因此24位的输入除以6得到4个Base64字符。

你在评论中问编码123456的大小是多少。请记住,该字符串的每个字符都是1个字节,即8位(假设为ASCII/UTF8编码),因此我们正在编码6个字节或48位的数据。根据公式,我们预计输出长度为(6字节/3字节)*4个字符=8个字符

123456放入Base64编码器中会创建一个8个字符长的字符串MTIzNDU2,正如我们所预期的那样。


6
使用此公式时,请注意它不提供填充后的长度。因此,可能会得到更长的长度。 - Spilarix
为了计算base64文本的预期解码字节数,我使用公式 floor((3 * (length - padding)) / 4)。请查看以下gist - Kurt Vangraefschepe

23

整数

一般来说,我们不希望使用双精度浮点数,因为我们不想使用浮点运算、舍入误差等。它们是不必要的。

为此,记住如何执行向上取整除法是一个好主意:ceil(x / y) 可以用双精度浮点数写成 (x + y - 1) / y(避免负数,但要注意溢出)。

可读性

如果你追求可读性,当然也可以像这样编程(Java示例,对于C语言你当然可以使用宏):

public static int ceilDiv(int x, int y) {
    return (x + y - 1) / y;
}

public static int paddedBase64(int n) {
    int blocks = ceilDiv(n, 3);
    return blocks * 4;
}

public static int unpaddedBase64(int n) {
    int bits = 8 * n;
    return ceilDiv(bits, 6);
}

// test only
public static void main(String[] args) {
    for (int n = 0; n < 21; n++) {
        System.out.println("Base 64 padded: " + paddedBase64(n));
        System.out.println("Base 64 unpadded: " + unpaddedBase64(n));
    }
}

内联

填充

我们知道每3个字节(或更少)需要每次使用4个字符块。因此,该公式变为(对于x = n和y = 3):

blocks = (bytes + 3 - 1) / 3
chars = blocks * 4

或者合并:

chars = ((bytes + 3 - 1) / 3) * 4

你的编译器将优化掉3 - 1,因此只需保留它以保持可读性。

未加填充的

不太常见的是未加填充的变体,在这种情况下,我们需要记住每6位需要一个字符,向上取整:

bits = bytes * 8
chars = (bits + 6 - 1) / 6

或者组合:

chars = (bytes * 8 + 6 - 1) / 6

然而,如果我们想要的话,仍然可以将其除以二:

chars = (bytes * 4 + 3 - 1) / 3

难以阅读

如果您不信任编译器为您进行最终优化(或者希望让同事感到困惑):

填充的

((n + 2) / 3) << 2

未填充的

((n << 2) | 2) / 3

所以,这就是两种逻辑计算的方式,我们不需要任何分支、位运算或模运算 - 除非我们真的需要。

注意事项:

  • 显然,您可能需要在计算中添加1来包括空终止字节。
  • 对于Mime类型,您可能需要注意可能的行终止字符等(寻找其他答案处理此问题)。

18

(为了给出简洁而完整的推导。)

每个输入字节有8位,因此对于n个输入字节,我们得到:

n × 8      输入比特

每6位是一个输出字节,因此:

ceil(n × 8 / 6)  =  ceil(n × 4 / 3)      输出字节

这是没有填充的情况。

有填充的情况下,我们将其舍入为四的倍数的输出字节:

ceil(ceil(n × 4 / 3) / 4) × 4  =  ceil(n × 4 / 3 / 4) × 4  =  ceil(n / 3) × 4      输出字节

参见嵌套除法(维基百科)以获取第一个等式。

使用整数算术,可以将ceil(n / m)计算为(n + m – 1) div m,因此我们得到:

(n * 4 + 2) div 3      没有填充

(n + 2) div 3 * 4      有填充

举例说明:

 n   with padding    (n + 2) div 3 * 4    without padding   (n * 4 + 2) div 3 
------------------------------------------------------------------------------
 0                           0                                      0
 1   AA==                    4            AA                        2
 2   AAA=                    4            AAA                       3
 3   AAAA                    4            AAAA                      4
 4   AAAAAA==                8            AAAAAA                    6
 5   AAAAAAA=                8            AAAAAAA                   7
 6   AAAAAAAA                8            AAAAAAAA                  8
 7   AAAAAAAAAA==           12            AAAAAAAAAA               10
 8   AAAAAAAAAAA=           12            AAAAAAAAAAA              11
 9   AAAAAAAAAAAA           12            AAAAAAAAAAAA             12
10   AAAAAAAAAAAAAA==       16            AAAAAAAAAAAAAA           14
11   AAAAAAAAAAAAAAA=       16            AAAAAAAAAAAAAAA          15
12   AAAAAAAAAAAAAAAA       16            AAAAAAAAAAAAAAAA         16

最后,在MIME Base64编码的情况下,每76个输出字节需要两个额外的字节(CR LF),四舍五入或向上取整,具体取决于是否需要终止换行符。


关于CR LF所需的额外字节,这是一个非常好的观点。当我为openssl生成的base64编码字符串分配缓冲区时,我忽略了它们。 - mkk

6

这里有一个函数,可以计算Base64编码文件的原始大小,以KB为单位:

private Double calcBase64SizeInKBytes(String base64String) {
    Double result = -1.0;
    if(StringUtils.isNotEmpty(base64String)) {
        Integer padding = 0;
        if(base64String.endsWith("==")) {
            padding = 2;
        }
        else {
            if (base64String.endsWith("=")) padding = 1;
        }
        result = (Math.ceil(base64String.length() / 4) * 3 ) - padding;
    }
    return result / 1000;
}

5
我认为给出的答案没有解决原问题,即针对长度为n字节的二进制字符串需要分配多少空间才能容纳base64编码。
答案是(floor(n / 3) + 1) * 4 + 1
这包括填充和一个终止的null字符。如果您进行整数运算,则可能不需要floor调用。
包括填充,在每个三字节块中,原始字符串的每个四个字节将转换为base64字符串,包括任何部分块。在字符串末尾有一到两个额外的字节时,添加填充后仍会转换为base64字符串中的四个字节。除非您有非常特殊的需求,否则最好添加填充,通常是等号字符。在C语言中,我添加了一个额外的字节,以代表null字符,因为没有null字符的ASCII字符串有一定的危险并且您需要单独携带字符串长度。

6
你的公式有误。考虑n=3时,期望结果(不包括空值填充)为4,但是你的公式返回8。 - CodesInChaos
5
我认为包含空终止符是愚蠢的,特别是因为我们在这里谈论的是.Net。 - CodesInChaos
在Windows中使用CryptBinaryToStringA可以正常工作。我投赞成票。 - TarmoPikaro

5

对于所有使用C语言的人,请看一下这两个宏:

// calculate the size of 'output' buffer required for a 'input' buffer of length x during Base64 encoding operation
#define B64ENCODE_OUT_SAFESIZE(x) ((((x) + 3 - 1)/3) * 4 + 1) 

// calculate the size of 'output' buffer required for a 'input' buffer of length x during Base64 decoding operation
#define B64DECODE_OUT_SAFESIZE(x) (((x)*3)/4) 

该内容摘自此处


4

我没有在其他回复中看到简化公式。逻辑已经覆盖,但我需要一个最基本的形式用于我的嵌入式使用:

  Unpadded = ((4 * n) + 2) / 3

  Padded = 4 * ((n + 2) / 3)

注意:计算未填充计数时,我们向上取整整数除法,即加上除数-1,本例中为+2。


3

当其他人还在争论代数公式时,我更愿意直接使用BASE64来告诉我:

$ echo "Including padding, a base64 string requires four bytes for every three-byte chunk of the original string, including any partial chunks. One or two bytes extra at the end of the string will still get converted to four bytes in the base64 string when padding is added. Unless you have a very specific use, it is best to add the padding, usually an equals character. I added an extra byte for a null character in C, because ASCII strings without this are a little dangerous and you'd need to carry the string length separately."| wc -c

525

$ echo "Including padding, a base64 string requires four bytes for every three-byte chunk of the original string, including any partial chunks. One or two bytes extra at the end of the string will still get converted to four bytes in the base64 string when padding is added. Unless you have a very specific use, it is best to add the padding, usually an equals character. I added an extra byte for a null character in C, because ASCII strings without this are a little dangerous and you'd need to carry the string length separately." | base64 | wc -c

看起来,一个3字节的公式可以用4个base64字符表示,这是正确的。


1
我对需要大量内存和 CPU 时间的计算有所反感,而这些计算可以在 1 纳秒和一两个寄存器中完成。 - Maarten Bodewes
那么,当您尝试处理未知数量的二进制数据时,这有什么帮助呢? - UKMonkey
这个问题主要是关于公式的,它可以帮助计算输出大小,而不需要进行base64本身的计算。虽然这个答案在某些情况下很有用,但它并不能解决这个问题。 - Alejandro

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