大家好,我在用 Rust 给客户的请求日志格式写一个小解码器(先是一个计数字节,然后是这么多个键值对,每个都是一个长度字节加上对应的字节)。字节从 socket 里以任意大小的块到达,所以 main 里维护一个 Vec<u8>,每来一块就追加,反复调用 next_record 直到返回 Incomplete,再把解析器说它用掉的部分 drain 掉。
整个数据作为一块喂进去,三条记录都能正常打印。把 chunk 大小作为第一个参数设成 7 或 10,就会以两种方式出错。一种是某条记录里冒出多余的字段(一条从没解析完的记录里的某个 key 出现在了下一条记录里),另一种是在单块运行能通过的字节上报 malformed at offset N。这两种现象似乎不是在同样的 chunk 大小下出现的,所以我分不清是一个 bug 还是两个。
我一直怀疑是 drain 的问题,但改了它只是换了一种垃圾输出。我还怀疑过在循环里一直持有 &buf(能编译通过,所以我觉得不太可能)。我把 Incomplete 当成“不是错误,等着就行”,循环也是这么处理的,所以我不明白还缺了什么。
fn take<'a>(&mut self, buf: &'a [u8], n: usize) -> Result<&'a [u8], ParseError> {
if self.pos + n > buf.len() {
return Err(ParseError::Incomplete);
}
let s = &buf[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn take_str(&mut self, buf: &[u8]) -> Result<String, ParseError> {
let len = self.take(buf, 1)?[0] as usize;
let raw = self.take(buf, len)?;
std::str::from_utf8(raw)
.map(|s| s.to_string())
.map_err(|_| ParseError::Malformed { offset: self.pos - len })
}
pub fn next_record(&mut self, buf: &[u8]) -> Result<Record, ParseError> {
let count = self.take(buf, 1)?[0] as usize;
if count == 0 || count > self.max_fields {
return Err(ParseError::Malformed { offset: self.pos - 1 });
}
for _ in 0..count {
let k = self.take_str(buf)?;
let v = self.take_str(buf)?;
self.scratch.push((k, v));
}
Ok(Record { fields: std::mem::take(&mut self.scratch) })
}
main 里的循环如下(Parser 里有 pos、scratch: Vec<(String, String)> 和 max_fields)。
for piece in data.chunks(chunk) {
buf.extend_from_slice(piece);
loop {
match parser.next_record(&buf) {
Ok(rec) => { seen += 1; println!("record {}: {:?}", seen, rec.fields); }
Err(ParseError::Incomplete) => break,
Err(ParseError::Malformed { offset }) => {
eprintln!("malformed at offset {}", offset);
return;
}
}
}
buf.drain(..parser.pos());
parser.reset_pos();
}
我到底漏了什么?
从英语翻译·查看原文hi all, writing a small decoder in Rust for a client's request-log format (count byte, then that many key/value pairs, each a length byte plus bytes). bytes arrive from a socket in arbitrary chunks, so main keeps a Vec<u8>, appends each chunk, calls next_record until it gets Incomplete, then drains what the parser says it used.
fed as one chunk, all three records print fine. with a chunk size of 7 or 10 as the first arg, it goes wrong in two ways: a record sometimes shows up with extra fields (a key from a record that never finished appears inside the next one), and other times I get malformed at offset N on bytes the one-chunk run accepts. the two symptoms don't seem to show up at the same chunk sizes, so I can't tell if it's one bug or two.
I've been blaming the drain, but changing it only changes which garbage I get. I also wondered about holding &buf across the loop (it compiles, so I doubt it). I treat Incomplete as "not an error, just wait", which is what the loop does, so I don't see what's missing.
fn take<'a>(&mut self, buf: &'a [u8], n: usize) -> Result<&'a [u8], ParseError> {
if self.pos + n > buf.len() {
return Err(ParseError::Incomplete);
}
let s = &buf[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn take_str(&mut self, buf: &[u8]) -> Result<String, ParseError> {
let len = self.take(buf, 1)?[0] as usize;
let raw = self.take(buf, len)?;
std::str::from_utf8(raw)
.map(|s| s.to_string())
.map_err(|_| ParseError::Malformed { offset: self.pos - len })
}
pub fn next_record(&mut self, buf: &[u8]) -> Result<Record, ParseError> {
let count = self.take(buf, 1)?[0] as usize;
if count == 0 || count > self.max_fields {
return Err(ParseError::Malformed { offset: self.pos - 1 });
}
for _ in 0..count {
let k = self.take_str(buf)?;
let v = self.take_str(buf)?;
self.scratch.push((k, v));
}
Ok(Record { fields: std::mem::take(&mut self.scratch) })
}
and the loop in main (Parser holds pos, scratch: Vec<(String, String)> and max_fields):
for piece in data.chunks(chunk) {
buf.extend_from_slice(piece);
loop {
match parser.next_record(&buf) {
Ok(rec) => { seen += 1; println!("record {}: {:?}", seen, rec.fields); }
Err(ParseError::Incomplete) => break,
Err(ParseError::Malformed { offset }) => {
eprintln!("malformed at offset {}", offset);
return;
}
}
}
buf.drain(..parser.pos());
parser.reset_pos();
}
what am I missing?