try_get_int 把 -1 读成 16777215
主线位置:共享主线的下游,
Buf读游标。读一个「n 字节的有符号整数」是协议解析里的常见操作(3 字节的偏移、24 位的采样值),get_int和try_get_int本该只差「数据不够时 panic 还是返回Err」。
flowchart TB gi["get_int n"] --> gu["get_uint n"] gu --> se["sign_extend"] se --> neg["-1"] ti["try_get_int n"] --> mac["buf_try_get_impl be"] mac --> pad["拷进全零的 8 字节"] pad --> fb["i64::from_be_bytes"] fb --> pos["16777215"]
get_int 的符号扩展怎么做、try_get_int 为什么绕过了它、这段代码修过几次,在 Buf 的「get_int:符号扩展」和那里的冷知识里讲过。这一页是它的使用者视角:哪些方法、哪些版本受影响,怎么绕开,怎么知道升级之后修好了。
本页的实验在 M2 Max、rustc 1.99.0、--release 下运行,依赖 bytes = "=1.12.1"。
复现
use bytes::{Buf, Bytes, TryGetError};
// the workaround: read unsigned, then sign-extend like get_int does
fn try_get_int_signed<B: Buf>(buf: &mut B, nbytes: usize) -> Result<i64, TryGetError> {
let v = buf.try_get_uint(nbytes)?;
if nbytes == 0 {
return Ok(0);
}
let shift = (8 - nbytes) * 8;
Ok(((v << shift) as i64) >> shift)
}
fn main() {
let cases: [(&[u8], usize); 5] = [
(&[0xff, 0xff, 0xff], 3),
(&[0x80, 0x00, 0x00], 3),
(&[0x7f, 0xff, 0xff], 3),
(&[0xfe], 1),
(&[0xff; 8], 8),
];
println!("{:<19} {:>9} {:>12} {:>10} {:>14} {:>10}", "hex, n", "get_int", "try_get_int", "get_int_le", "try_get_int_le", "workaround");
for (data, n) in cases {
let be = (&data[..]).get_int(n);
let try_be = (&data[..]).try_get_int(n).unwrap();
// reverse so the little-endian readers see the same number
let rev: Vec<u8> = data.iter().rev().copied().collect();
let le = (&rev[..]).get_int_le(n);
let try_le = (&rev[..]).try_get_int_le(n).unwrap();
let fixed = try_get_int_signed(&mut &data[..], n).unwrap();
println!("{:<19} {be:>9} {try_be:>12} {le:>10} {try_le:>14} {fixed:>10}", format!("{}, {n}", data.iter().map(|b| format!("{b:02x}")).collect::<String>()));
}
// native endian on this machine, and other Buf types
let ne = [0xffu8, 0xff, 0xff];
println!("get_int_ne(3) = {}, try_get_int_ne(3) = {:?}", (&ne[..]).get_int_ne(3), (&ne[..]).try_get_int_ne(3));
let mut b = Bytes::from_static(&[0xff, 0xff, 0xff]);
println!("Bytes: try_get_int(3) = {:?}", b.try_get_int(3));
let mut ch = (&[0xffu8][..]).chain(&[0xffu8, 0xff][..]);
println!("Chain: try_get_int(3) = {:?}", ch.try_get_int(3));
println!("fixed width: try_get_i16 = {:?}, try_get_i32 = {:?}", (&[0xffu8; 2][..]).try_get_i16(), (&[0xffu8; 4][..]).try_get_i32());
let mut s = &[0xffu8, 0xff][..];
println!("short: try_get_int(3) = {:?}, workaround = {:?}", s.try_get_int(3), try_get_int_signed(&mut s, 3));
}
跑 3 次,输出相同:
hex, n get_int try_get_int get_int_le try_get_int_le workaround
ffffff, 3 -1 16777215 -1 16777215 -1
800000, 3 -8388608 8388608 -8388608 8388608 -8388608
7fffff, 3 8388607 8388607 8388607 8388607 8388607
fe, 1 -2 254 -2 254 -2
ffffffffffffffff, 8 -1 -1 -1 -1 -1
get_int_ne(3) = -1, try_get_int_ne(3) = Ok(16777215)
Bytes: try_get_int(3) = Ok(16777215)
Chain: try_get_int(3) = Ok(16777215)
fixed width: try_get_i16 = Ok(-1), try_get_i32 = Ok(-1)
short: try_get_int(3) = Err(TryGetError { requested: 3, available: 2 }), workaround = Err(TryGetError { requested: 3, available: 2 })
哪些受影响,从表里读出来:
| 情形 | 结果 |
|---|---|
try_get_int / try_get_int_le / try_get_int_ne,nbytes 小于 8,最高位为 1 | 错:得到 2^(8·nbytes) 加上正确值,例如 -1 成了 16777215、-8388608 成了 8388608 |
| 同上,最高位为 0(非负数) | 对 |
同上,nbytes 等于 8 | 对:8 个字节填满 i64,不需要扩展 |
get_int / get_int_le / get_int_ne | 对 |
定长的 try_get_i8 / i16 / i32 / i64(及 _le / _ne) | 对:数组本身就是该类型的宽度 |
底层是 &[u8]、Bytes、Chain,或经 &mut B 转发 | 都一样错 |
只有负数出错、非负数都对,所以拿正数写的测试全部通过。这是它能一直留到现在的原因之一,见下面的问答。
源码
try_get_int 和 try_get_int_le 直接用 buf_try_get_impl!:
fn try_get_int(&mut self, nbytes: usize) -> Result<i64, TryGetError> {
buf_try_get_impl!(be => self, i64, nbytes);
}
fn try_get_int_le(&mut self, nbytes: usize) -> Result<i64, TryGetError> {
buf_try_get_impl!(le => self, i64, nbytes);
}
try_get_int_ne 按目标平台的字节序转给这两个之一。宏的 be 分支 把 nbytes 个字节拷进一个全零的 8 字节数组的末尾,再整体转换:
let mut buf = [0; SIZE];
$this.try_copy_to_slice(&mut buf[slice_at..])?;
return Ok($typ::from_be_bytes(buf));
高位那几个字节是 0,i64::from_be_bytes 得到的就是非负数。le 分支同理,只是拷进数组的开头。这个宏对 try_get_uint 是对的(无符号数高位本来就该补 0),对有符号数少了一步。对照 get_int:
fn get_int(&mut self, nbytes: usize) -> i64 {
sign_extend(self.get_uint(nbytes), nbytes)
}
它先读无符号值,再交给 sign_extend 把符号位复制到高位。
Buf 的实现者里没有谁覆盖这几个方法:在 1.12.1 的 src/ 里,fn try_get_int 只出现在 trait 的默认实现和 deref_forward_buf! 的转发里(原样调用 (**self).try_get_int(nbytes),用于 &mut T 和 Box<T>)。所以 Bytes、BytesMut、Chain、&mut B、Box<B> 全都走同一份代码,上面的输出也是这样。
问: 这么明显的错,文档测试为什么没抓到?
答: 1.12.1 里 try_get_int 和 try_get_int_le 各有两个文档示例:一个读 b"\x01\x02\x03 hello"(小端版是 b"\x03\x02\x01 hello"),断言 Ok(0x010203_i64);另一个测数据不够时的 Err。一个是正数,一个是错误路径,而正数恰好不受影响。PR #839 的说明里写,新增的测试 try_get_int_sign_extends 覆盖了大端、小端、本机字节序的符号扩展,并且「It fails on master (try_get_int(3) returns Ok(16777215)) and passes with this change」。
哪些版本受影响
try_get_*这一族是 1.10.0 加入的(#753,git tag --contains的第一个 tag 是v1.10.0),try_get_int从那时起就是现在这样。所以 1.10.0 到 1.12.1 都受影响;更早的版本没有这些方法。修复是 PR #839(「fix: try_get_int/try_get_int_le do not sign-extend n-byte integers」),2026-09-03 合并,合并提交
7930d93。它把两个方法改成和get_int一样的写法:fn try_get_int(&mut self, nbytes: usize) -> Result<i64, TryGetError> { Ok(sign_extend(self.try_get_uint(nbytes)?, nbytes)) }PR 说明还写道:「
try_get_int_nedelegates to these two methods and inherited the bug; it is fixed transitively. NoBufimplementation overrides these methods, so all implementors were affected.」写作时(2026-10-24)没有任何 tag 包含
7930d93:git tag --contains 7930d93输出为空,GitHub 上最新的 release 仍是 2026-07-08 的v1.12.1,master 比v1.12.1多 4 个提交(见 怎么读这一卷 的「锁定版本」)。
修法
- 用上面的
try_get_int_signed:先try_get_uint,再做和sign_extend一样的移位。它在数据不够时返回同样的TryGetError、不动游标(因为失败的是try_get_uint,见 Buf 的「try_get_*:不想 panic 时」),nbytes == 0返回 0,nbytes > 8时由try_get_uintpanic,和原方法的约定一致。小端用try_get_uint_le。 - 或者先比较
remaining() >= nbytes,够了再调get_int。 - 宽度正好是 1、2、4、8 字节时,直接用定长的
try_get_i8/try_get_i16/try_get_i32/try_get_i64,它们不受影响。
这个替代写法在修复发版之后依然正确:修复后的 try_get_int 做的正是同一件事,到时候换回去即可。
怎么确认自己没踩
在自己的代码和依赖里搜
try_get_int(会同时匹配_le、_ne)。cargo tree -i bytes看锁定的版本是否在 1.10.0 到 1.12.1 之间。- 写一条现在应该失败、修复发版后会通过的测试,升级 bytes 时它会告诉你修好了没有:
use bytes::Buf; // red on bytes 1.12.1; turns green once a release contains #839 #[test] fn try_get_int_sign_extends() { assert_eq!((&[0xffu8, 0xff, 0xff][..]).try_get_int(3), Ok(-1)); assert_eq!((&[0xffu8, 0xff, 0xff][..]).try_get_int_le(3), Ok(-1)); }在 1.12.1 上
cargo test --release跑 3 次,除线程号外相同:running 1 test try_get_int_sign_extends --- FAILED failures: ---- try_get_int_sign_extends stdout ---- thread 'try_get_int_sign_extends' (1942153) panicked at tests/try_get_int_sign.rs:6:3: assertion `left == right` failed left: Ok(16777215) right: Ok(-1) note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: try_get_int_sign_extends test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s反过来也验过:把 bytes 换成
7930d93这个提交的源码(git archive导出,用path依赖),同一条测试通过,test result: ok. 1 passed。所以这条测试确实能区分修没修。 自己的协议测试里,给每个有符号字段至少放一个负数样例(最高位为 1 的值,如
0x800000、0xffffff)。
小结
- bytes 1.10.0 到 1.12.1 的
try_get_int、try_get_int_le、try_get_int_ne在nbytes < 8时不做符号扩展,负数读成正数(0xffffff得到 16777215);get_int系列和定长的try_get_i16等都对。 - 原因是它们直接用
buf_try_get_impl!把字节补零转换,没有经过sign_extend;没有任何Buf实现覆盖它们,所有类型都受影响。 - 修复 #839 在 2026-09-03 合进 master,写作时没有 tag 包含它。
- 绕开:
try_get_uint加手动符号扩展,或先查remaining()再get_int;留一条用负数断言的测试,它在修复发版后才会变绿。