java byte array 转String在转回byte array不相等

背景

最近在搞微软的NBFS协议,这个协议实际上也是基于WebService,只不过对xml进行了压缩,按照他自己的编码规则进行压缩
网上搜罗一圈后发现有个大佬写好的burpNBFS插件WCF-Binary-SOAP-Plug-In
这个插件会将传入的经过base64编码的xml转换成NBFS协议的base64编码的字符串

测试那边要使用jmeter对这个NBFS接口性能测试
基本思路:

  • 新建http request输入原始的xml
  • 搞一个PreProcessor,调用大佬写的NBFS.exe获取压缩后的XMLbase64编码的字符串

PreProcessor 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.commons.codec.binary.Base64;

// 获取请求体数据
def requestBody = sampler.getArguments().getArgument(0).getValue();

// 检查请求体是否存在
if (requestBody != null) {
log.info("获取到请求体:{}",requestBody)
// 进行加密操作,这里使用Base64编码作为示例
def reqBase64Str = Base64.encodeBase64String(requestBody.getBytes());
def process = Runtime.getRuntime().exec("./NBFS.exe","encode",reqBase64Str)
process.waitFor()
def nbfsEncodeBase64Str = process.inputStream.text
def result = Base64.decodeBase64(nbfsEncodeBase64Str)
// 将加密后的数据设置回请求体
sampler.getArguments().getArgument(0).setValue(new String(result));
} else {
log.warn("请求体不存在");
}

这时候就会发现一个神奇的东西,接口会返回400错误

问题分析

先说明下NBFS编码的原理,为了压缩xml,NBFS会预先定义一些字典,比如:0x90代表Reason这个字符,
也就是说,经过NBFS.exe返回的Base64字符串经过Base64.decodeBase64(nbfsEncodeBase64Str)这个方法返回的byte数组中可能出现0x80,0xAA等字节

众所周知,java默认的字符串编码是UTF-8

另一个常识是:java字符串是由Char[]表示的,而Char是由unicode表示

ok,有了上面的已知条件,返回400的问题,就是解释为什么下面代码输出是false就行了

1
2
3
4
byte[] byteArray = new byte[]{0x01, (byte)0x81, (byte)0xAA, 0x44, 0x45};
String str = new String(byteArray);
byte [] revertByteArray = str.getBytes();
System.out.println(Arrays.equals(byteArray,revertByteArray));

其实断点调试下,会发现其实revertByteArray这个数组其实是:[1, -17, -65, -67, -17, -65, -67, 68, 69]而原数组是:[1, -128, -86, 68, 69]
区别就是多出了6个byte,其中0x81,0xAA对应变成[-17,-65,-67],对应10进制的65533,对应的unicde就是:\uFFFD

接下来就分析一下为什么0x81为什么会变成[-17,-65,-67]这玩意就行了

debug大法

new String(byteArr)这段代码开始debug,
就会发现最终会进入一个超长的方法:

sun.nio.cs.UTF_8.Decoder#decode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// da 就是字符串找保存的char[]
// sa 就是String构造函数传进来的byte 数组
// 就是这个方法将byte [] -> char []
// sp 是0,应该是偏移量, String str = new String(byteArray,1,1,"UTF-8");这么创建字符串才会有,这个不重要,
// len就是byte数组的长度
public int decode(byte[] sa, int sp, int len, char[] da) {
final int sl = sp + len;
int dp = 0;
int dlASCII = Math.min(len, da.length);
ByteBuffer bb = null; // only necessary if malformed

// ASCII only optimized loop
//性能优化,>= 0的意思就是byte没有溢出,也就是:0~128,对应就是0x00 到 0x00 0x80
while (dp < dlASCII && sa[sp] >= 0)
da[dp++] = (char) sa[sp++];

//下面就是范围判断,判断byte是否在utf8的编码范围内
while (sp < sl) {
int b1 = sa[sp++];
if (b1 >= 0) {
// 1 byte, 7 bits: 0xxxxxxx
da[dp++] = (char) b1;
} else if ((b1 >> 5) == -2 && (b1 & 0x1e) != 0) {
// 2 bytes, 11 bits: 110xxxxx 10xxxxxx
.
.
.
} else if ((b1 >> 4) == -2) {
// 3 bytes, 16 bits: 1110xxxx 10xxxxxx 10xxxxxx
.
.
.
} else if ((b1 >> 3) == -2) {
// 4 bytes, 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
.
.
.
} else {
if (malformedInputAction() != CodingErrorAction.REPLACE)
return -1;
da[dp++] = replacement().charAt(0);
}
}
return dp;
}

上面代码翻译成人话就是:
循环byte数组里面的每一个byte

  1. 如果>=0,就是ASCII
  2. 如果不是,判断是否符合条件:
    1. 1 byte, 7 bits: 0xxxxxxx
    2. 2 bytes, 11 bits: 110xxxxx 10xxxxxx
    3. 3 bytes, 16 bits: 1110xxxx 10xxxxxx 10xxxxxx
    4. 4 bytes, 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  3. 如果都不符合,就返回replacement()第0个char(打断点看,这个字符其实就是main类启动时创建的,其实这就是’65533’也就是\uFFFD)

至此,真相大白

0x80->0b10000000
0x88->0b10010000

都不在上面的范围,所以他们最终的char最终都会变成65533(\uFFFD)

其实上面的规则就是UTF-8规则,参考维基百科:

在ASCII码的范围,用一个字节表示,超出ASCII码的范围就用字节表示,这就形成了我们上面看到的UTF-8的表示方法,这样的好处是当UNICODE文件中只有ASCII码时,存储的文件都为一个字节,所以就是普通的ASCII文件无异,读取的时候也是如此,所以能与以前的ASCII文件兼容。
大于ASCII码的,就会由上面的第一字节的前几位表示该unicode字符的长度,比如110xxxxx前三位的二进制表示告诉我们这是个2BYTE的UNICODE字符;1110xxxx是个三位的UNICODE字符,依此类推;xxx的位置由字符编码数的二进制表示的位填入。越靠右的x具有越少的特殊意义。只用最短的那个足够表达一个字符编码数的多字节串。注意在多字节串中,第一个字节的开头”1”的数目就是整个串中字节的数目。

太长不看

一句话总结:因为类似0x80,0x88等字节不在utf-8编码范围内,所以会返回一个默认字符,\uFFFD(65533),这个字符占3个字节,所以就造成的两个byte数组不相等

解决方法

解决方法很简单:
找一个编码覆盖0x000xff,并且只用一个字节编码的编码格式就行,
也就是ISO-8859-1

所以下面代码输出就是true

1
2
3
4
byte[] byteArray = new byte[]{0x01, (byte)0x81, (byte)0xAA, 0x44, 0x45};
String str = new String(byteArray,"ISO-8859-1");
byte [] revertByteArray = str.getBytes("ISO-8859-1");
System.out.println(Arrays.equals(byteArray,revertByteArray));

java byte array 转String在转回byte array不相等

https://fingergohappy.github.io/2023/09/21/java-byte-convert-string-not-equal/

Author

finger

Posted on

2023-09-21

Updated on

2025-06-11

Licensed under

Comments