GLib(Vala)中DataInputStream异常Skip行为排查(NBS文件场景)
我正在为.NBS(Note Block Studio歌曲文件)编辑器编写辅助类,在实现将.NBS文件加载至结构体的逻辑时,遇到了DataInputStream的异常skip行为。例如,若作者名为"Wattana",读取后会显示为"attana",必须将skip字节数改为3才能正常读取。为何读取song_name前skip4字节正常,而读取song_author前skip4字节会跳过字符串首字符?以下是辅助类的部分代码:
public static SongData to_song_data (DataInputStream data_stream) { // Specifications for the NBS file format can be found at https://opennbs.org/nbs data_stream.set_byte_order (DataStreamByteOrder.LITTLE_ENDIAN); var song_data = new SongData(); // Note to self: a "short" is 2 bytes(16 bits) long. // TODO: Add error handler. song_data.old_version = data_stream.read_int16 (); song_data.version = data_stream.read_byte (); song_data.vanilla_intrument_count = data_stream.read_byte (); song_data.length = data_stream.read_int16 (); song_data.layer_count = data_stream.read_int16 (); data_stream.skip (4); // 4 bytes(32 bits) must be skipped before reading any string from an NBS file. song_data.song_name = data_stream.read_line (); data_stream.skip (3); // Not sure why 4 bytes skip doesn't work here. song_data.song_author = data_stream.read_line (); return song_data; }
嘿,这个问题的核心其实是你对NBS文件格式中字符串字段的理解出现了偏差,再加上skip()方法的行为和你的预期不匹配导致的。让我一步步拆解:
1. NBS字符串字段的正确结构
根据NBS格式规范,每个字符串字段并不是以固定4字节偏移开头,而是遵循以下结构:
- 一个2字节的小端序short(16位整数),表示后续UTF-8字符串的字节长度
- 紧接着是对应长度的UTF-8编码字节数据
你代码里注释的“读取字符串前必须跳过4字节”是错误的,这也是问题的根源。
2. 为什么第一次skip(4)看似正常?
我们来计算一下读取到layer_count后的字节偏移:
old_version(2字节) +version(1字节) +vanilla_intrument_count(1字节) +length(2字节) +layer_count(2字节) = 总共8字节
而NBS规范中,layer_count之后直接就是song_name的2字节长度前缀。你这里skip(4)相当于跳过了song_name的长度前缀(2字节)加上song_name的前2字节。这时候你的read_line()方法能读出正确的song_name,大概率是因为这个方法的实现逻辑有问题——比如它可能是读取到某个终止符(而非按长度前缀读取),刚好巧合地跳过了被截断的部分后读到了完整内容。
3. 为什么第二次skip(4)会丢失首字符?
当你读完song_name后,文件指针已经停在song_name的末尾,紧接着就是song_author的2字节长度前缀。这时候你再skip(4),就会跳过:
song_author的2字节长度前缀 +song_author的前2字节
当你用read_line()读取时,自然就会丢失首字符(比如"Wattana"变成"attana",刚好丢了第一个字节)。而改成skip(3)时,只是巧合地少跳了1字节,刚好对齐到了字符串的起始位置,但这并不是正确的解决方式。
4. 正确的实现方式
你需要替换掉错误的skip()和read_line()逻辑,改为按照NBS规范读取字符串:
import java.io.IOException; import java.nio.charset.StandardCharsets; // 读取NBS字符串的工具方法 private static String readNbsString(DataInputStream dataStream) throws IOException { short length = dataStream.read_int16(); // 读取2字节长度前缀(小端序) if (length <= 0) { return ""; } byte[] strBytes = new byte[length]; dataStream.readFully(strBytes); // 确保读取完整的字符串字节 return new String(strBytes, StandardCharsets.UTF_8); } // 修改后的to_song_data方法 public static SongData to_song_data (DataInputStream data_stream) throws IOException { data_stream.set_byte_order (DataStreamByteOrder.LITTLE_ENDIAN); var song_data = new SongData(); song_data.old_version = data_stream.read_int16 (); song_data.version = data_stream.read_byte (); song_data.vanilla_intrument_count = data_stream.read_byte (); song_data.length = data_stream.read_int16 (); song_data.layer_count = data_stream.read_int16 (); // 去掉所有skip操作,直接按规范读取字符串 song_data.song_name = readNbsString(data_stream); song_data.song_author = readNbsString(data_stream); return song_data; }
额外提醒:skip()方法的坑
另外要注意,DataInputStream.skip(long n)方法不保证一定会跳过n字节,它返回的是实际跳过的字节数。如果文件剩余字节不足n,它只会跳过剩余的部分。这也是为什么有时候你的skip()行为会出现看似随机的异常——你假设它跳过了4字节,但实际可能并没有。
内容的提问来源于stack exchange,提问作者user9102859

