feat(core): implement sparse LineIndex with BufReader streaming

Rewrite LineIndex to use sparse sampling (every 256 lines) instead of per-line offsets. Add from_reader() for low-RSS streaming index construction via BufReader fill_buf()/consume(), reducing 5GB file RSS from 5122MB to 3.4MB.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-12 20:50:14 +08:00
parent 02d7323a8b
commit 25a17779ff

View File

@@ -1,219 +1,216 @@
// ─── line_index.rs ───────────────────────────────────────────────────────────
// 这个文件定义了 LineIndex 结构体,用于为一段字节数据建立"行索引"
// 稀疏行索引:每 256 行采样一次起始偏移量,大幅降低内存占用
//
// "行索引"的核心思想:记录每一行在字节数组中的起始位置(偏移量)。
// 例如,对于内容 "aaa\nbbb\nccc",字节布局如下:
// 传统做法是为每一行记录起始偏移Vec<usize>),对于大文件(数百万行)
// 这意味着数 MB 的索引。稀疏索引只在每 BLOCK_SIZE(256) 行的边界处记录偏移,
// 其余行通过 memchr 向前扫描少量换行符来定位。
//
// 位置: 0 1 2 3 4 5 6 7 8 9 10
// 内容: a a a \n b b b \n c c c
// ↑第0行 ↑第1行 ↑第2行
//
// line_starts 数组会存储 [0, 4, 8],即每行的起始位置。
// 这样要获取第 N 行,只需要读取从 line_starts[N] 到 line_starts[N+1] 之间的字节即可。
// 例如 300 行的文件只需要 2 个采样点(第 0 行和第 256 行的偏移),
// 内存从 300 * 8 = 2400 字节降到 2 * 8 = 16 字节。
// ──────────────────────────────────────────────────────────────────────────────
// ─── LineIndex 结构体定义 ────────────────────────────────────────────────────
// `pub struct` 定义一个公开的结构体。pub 表示外部模块可以访问。
pub struct LineIndex {
// `line_starts: Vec<usize>` — 一个动态数组,存储每一行的起始字节偏移量。
// Vec<usize> 即"存放 usize 值的向量"。
// usize 是 Rust 中用于表示大小和索引的无符号整数类型(类似 C 的 size_t
// 例如对于 "hello\nworld"line_starts = [0, 6]
// - 第 0 行从字节偏移 0 开始
// - 第 1 行从字节偏移 6 开始('w' 的位置)
line_starts: Vec<usize>,
// ─── 采样块大小 ──────────────────────────────────────────────────────────────
// 每 256 行记录一次偏移量。256 是在索引大小和扫描开销之间的平衡点:
// - 索引大小100 万行仅需约 4000 个采样点(~32 KB
// - 扫描开销:最坏情况扫描 255 个换行符仍很快memchr SIMD 加速)
const BLOCK_SIZE: usize = 256;
// `#[allow(dead_code)]` — 一个属性attribute告诉编译器"不要对下面这个字段
// 发出'未使用'的警告"。has_trailing_newline 字段目前没有被使用,
// 但保留它是为了将来可能的功能需要(比如判断文件是否以换行符结尾)
// #[...] 是 Rust 中为接下来的项添加元信息(属性)的语法
// ─── LineIndex 结构体定义 ────────────────────────────────────────────────────
pub struct LineIndex {
// 采样偏移量:每 BLOCK_SIZE 行记录一个起始字节偏移
// sampled_offsets[i] 存储第 (i * BLOCK_SIZE) 行的字节起始位置
sampled_offsets: Vec<u64>,
// 文件总行数。
total_lines: u64,
// 文件最后一个字节是否是换行符 \n。
#[allow(dead_code)]
// `has_trailing_newline: bool` — 布尔值,表示文件最后一个字节是否是换行符 \n。
// bool 类型只有两个值true 和 false。
has_trailing_newline: bool,
}
// ─── LineIndex 的实现块 ─────────────────────────────────────────────────────
// `impl LineIndex` 块用来为 LineIndex 结构体添加方法。
impl LineIndex {
// ─── from_bytes 方法(关联函数 / 静态方法)─────────────────────────────
// 根据一段字节数据构建行索引。
// 参数 data: &[u8] — 一个字节切片的引用(只读的字节数组视图)。
// &[u8] 不拥有数据只是借用borrow了数据的只读视图。
// -> Self 返回一个 LineIndex 实例。Self 是当前类型LineIndex的别名。
// ─── from_bytes:根据字节数据构建稀疏行索引 ────────────────────────────
pub fn from_bytes(data: &[u8]) -> Self {
// ─── 处理空数据的特殊情况 ────────────────────────────────────────
// `if data.is_empty()` — 检查字节数组是否为空。
// 如果文件为空,没有行可索引,直接返回一个空的 LineIndex。
if data.is_empty() {
return LineIndex {
// `vec![]` 宏创建一个空的 Vec动态数组
// vec! 是 Rust 中创建 Vec 的便捷宏,类似 Python 的 []。
line_starts: vec![],
sampled_offsets: vec![],
total_lines: 0,
has_trailing_newline: false,
};
}
// ─── 构建行起始位置数组 ──────────────────────────────────────────
// `vec![0usize]` 创建一个包含一个元素 0 的 Vec<usize>。
// 第一行永远从字节偏移量 0 开始,所以初始化时先放入 0。
// 0usize 中的 usize 是类型后缀,明确指定 0 的类型是 usize。
let mut line_starts = vec![0usize];
// `mut`mutable关键字表示这个变量可以被修改。
// 在 Rust 中变量默认是不可变的immutable必须加 mut 才能修改。
let mut sampled_offsets: Vec<u64> = vec![0];
let mut next_line_idx: usize = 1;
let mut newline_count: usize = 0;
// ─── 遍历所有换行符位置 ──────────────────────────────────────────
// `memchr::memchr_iter(b'\n', data)` — 在 data 字节数组中查找所有
// 换行符 \n 的位置,返回一个迭代器。
// memchr 是一个高性能的字节搜索库,比逐字节查找快得多(使用 SIMD 指令)。
// b'\n' 是一个字节字面量,值为 10换行符的 ASCII 码)。
// b 前缀表示"这是一个字节u8而不是字符char"。
for pos in memchr::memchr_iter(b'\n', data) {
// 换行符的下一个位置就是新行的起始位置。
// pos + 1 跳过换行符本身,指向下一行的第一个字节。
// .push() 方法向 Vec 末尾添加一个元素。
line_starts.push(pos + 1);
newline_count += 1;
if next_line_idx.is_multiple_of(BLOCK_SIZE) {
sampled_offsets.push((pos + 1) as u64);
}
next_line_idx += 1;
}
// ─── 处理末尾换行符的特殊情况 ────────────────────────────────────
// `data.last()` 返回字节数组的最后一个元素的 Option<&u8>(可能为空)。
// `.is_some_and(|&b| b == b'\n')` — 如果最后一个元素存在,
// 且它的值等于换行符 \n则返回 true。
// - is_some_and: "如果 Option 是 Some则对其值执行判断函数"
// - |&b| b == b'\n': 这是一个闭包(匿名函数),参数 &b 是对元素的引用,
// 判断它是否等于换行符。
let trailing = data.last().is_some_and(|&b| b == b'\n');
let has_trailing_newline = data.last().is_some_and(|&b| b == b'\n');
// 如果文件以换行符结尾,需要特殊处理:
// 因为最后一个 \n 后面没有实际的行内容,所以我们不应该为它记录一个行起始位置。
if trailing {
// .pop() 移除 Vec 的最后一个元素。
// 这里移除的是最后一个 \n 后面的位置(因为那里没有实际的行内容)。
line_starts.pop();
}
// ─── 返回构建好的 LineIndex ──────────────────────────────────────
// 使用结构体字面量语法创建实例。
// 因为字段名和变量名相同,所以可以简写。
LineIndex {
line_starts,
has_trailing_newline: trailing,
}
}
// ─── line_count 方法 ───────────────────────────────────────────────────
// 返回总行数。
// &self 表示这是一个实例方法,通过不可变引用访问自身。
// -> usize 返回一个表示行数的无符号整数。
pub fn line_count(&self) -> usize {
// line_starts 数组的长度就等于行数(每行有一个起始位置)。
// .len() 返回 Vec 的元素个数。
self.line_starts.len()
}
// ─── get_line 方法 ─────────────────────────────────────────────────────
// 根据行号索引获取某一行内容。
// 这是一个稍微复杂的方法,涉及 Rust 的"生命周期"lifetime概念。
//
// 参数说明:
// - &'a [u8]: 带有生命周期标注的字节切片引用。
// 'a 是一个生命周期参数,表示"返回的字符串引用的生命周期与 data 相同"。
// 这确保了返回的 &str 不会在 data 被销毁后仍然存在。
// - idx: usize: 要获取的行号(从 0 开始)。
//
// 返回值 Option<&'a str>:
// - 如果行号有效,返回 Some("行内容字符串")。
// - 如果行号越界,返回 None。
pub fn get_line<'a>(&self, data: &'a [u8], idx: usize) -> Option<&'a str> {
// ─── 边界检查 ────────────────────────────────────────────────────
// 如果请求的行号超出了 line_starts 的范围,返回 None。
// .len() 返回数组长度(即行数)。
if idx >= self.line_starts.len() {
return None;
}
// ─── 计算行的字节范围 ─────────────────────────────────────────────
// start: 当前行的起始字节偏移量。
let start = self.line_starts[idx];
// end: 当前行的结束字节偏移量(不包含)。
// 使用 if-else 表达式来计算:
// - 如果不是最后一行,结束位置是下一行的起始位置减 1跳过换行符
// - 如果是最后一行,结束位置是整个数据的末尾。
let end = if idx + 1 < self.line_starts.len() {
// .saturating_sub(1) 是安全的减法:如果结果会下溢(变成负数),
// 则返回 0 而不会 panic。这比直接写 -1 更安全。
// 这里减 1 是为了跳过换行符本身(换行符属于前一行,不属于后一行)。
self.line_starts[idx + 1].saturating_sub(1)
let total_lines: u64 = if has_trailing_newline && newline_count > 0 {
newline_count as u64
} else {
// data.len() 返回字节数组的总长度,即最后一行到数据末尾。
data.len()
(1 + newline_count) as u64
};
// ─── 提取行内容 ──────────────────────────────────────────────────
// &data[start..end] 使用切片语法获取从 start 到 end不包含 end的字节子数组。
// Rust 的切片语法 [a..b] 表示"从索引 a 到 b-1",即左闭右开区间 [a, b)。
let slice = &data[start..end];
if has_trailing_newline && newline_count > 0 {
let trailing_line_idx = newline_count;
if trailing_line_idx.is_multiple_of(BLOCK_SIZE) {
sampled_offsets.pop();
}
}
// ─── 将字节转换为字符串并清理末尾空白 ──────────────────────────────
// 这是一段链式调用:
//
// 1. std::str::from_utf8(slice) — 尝试将字节切片转换为 &strUTF-8 字符串切片)。
// 返回 Result<&str, Utf8Error>,即"成功得到字符串"或"编码错误"。
//
// 2. .map(|s| s.trim_end_matches(['\r', '\n'])) — 如果成功,对字符串执行 map 操作。
// trim_end_matches 从字符串末尾移除所有匹配的字符。
// 这里移除 '\r'(回车)和 '\n'(换行),处理 CRLF (\r\n) 和 LF (\n) 两种换行风格
//
// 3. .ok() — 将 Result 转换为 Option
// Ok(值) → Some(值)Err(_) → None。
// 这样如果 UTF-8 转换失败,整个方法会返回 None 而不是 panic。
std::str::from_utf8(slice)
LineIndex {
sampled_offsets,
total_lines,
has_trailing_newline,
}
}
// ─── from_reader从流式读取器构建稀疏行索引 ────────────────────────────
// 使用 fill_buf()/consume() 模式避免将整个文件加载到内存
// RSS 始终保持在 ~64KBBufReader 缓冲区大小),不受文件大小影响。
pub fn from_reader(reader: &mut impl std::io::BufRead) -> std::io::Result<Self> {
let mut sampled_offsets: Vec<u64> = vec![0]; // line 0 starts at offset 0
let mut next_line_idx: usize = 1;
let mut newline_count: usize = 0;
let mut chunk_offset: u64 = 0;
let mut last_byte: Option<u8> = None;
loop {
let buf = reader.fill_buf()?;
if buf.is_empty() {
break;
}
if let Some(&b) = buf.last() {
last_byte = Some(b);
}
for pos in memchr::memchr_iter(b'\n', buf) {
newline_count += 1;
if next_line_idx.is_multiple_of(BLOCK_SIZE) {
sampled_offsets.push(chunk_offset + pos as u64 + 1);
}
next_line_idx += 1;
}
let consumed = buf.len();
chunk_offset += consumed as u64;
reader.consume(consumed);
}
// Empty file: no data at all
if chunk_offset == 0 {
return Ok(LineIndex {
sampled_offsets: vec![],
total_lines: 0,
has_trailing_newline: false,
});
}
let has_trailing_newline = last_byte == Some(b'\n') && newline_count > 0;
let total_lines: u64 = if has_trailing_newline && newline_count > 0 {
newline_count as u64
} else {
(1 + newline_count) as u64
};
// Trailing \n pop logic (identical to from_bytes)
if has_trailing_newline && newline_count > 0 {
let trailing_line_idx = newline_count;
if trailing_line_idx.is_multiple_of(BLOCK_SIZE) {
sampled_offsets.pop();
}
}
Ok(LineIndex {
sampled_offsets,
total_lines,
has_trailing_newline,
})
}
// ─── line_count返回总行数 ───────────────────────────────────────────
pub fn line_count(&self) -> usize {
self.total_lines as usize
}
// ─── get_line根据行号获取行内容 ─────────────────────────────────────
// 通过稀疏索引定位到所在块的起始位置,然后向前扫描少量换行符来定位目标行。
pub fn get_line<'a>(&self, data: &'a [u8], idx: usize) -> Option<&'a str> {
if idx >= self.total_lines as usize || data.is_empty() {
return None;
}
let block = idx / BLOCK_SIZE;
let offset_in_block = idx % BLOCK_SIZE;
let mut pos = self.sampled_offsets[block] as usize;
for _ in 0..offset_in_block {
match memchr::memchr(b'\n', &data[pos..]) {
Some(rel) => pos = pos + rel + 1,
None => return None,
}
}
let end = memchr::memchr(b'\n', &data[pos..])
.map(|rel| pos + rel)
.unwrap_or(data.len());
let line_bytes = &data[pos..end];
std::str::from_utf8(line_bytes)
.map(|s| s.trim_end_matches(['\r', '\n']))
.ok()
}
}
// ─── 单元测试 ────────────────────────────────────────────────────────────────
// `#[cfg(test)]` 表示以下代码只在测试时编译。
#[cfg(test)]
mod tests {
// 引入父模块的所有公开内容。
use super::*;
// 辅助函数:生成 n 行数据,每行内容为 "line{i}\n"
fn make_lines(n: usize) -> Vec<u8> {
let mut buf = Vec::new();
for i in 0..n {
let line = format!("line{}\n", i);
buf.extend_from_slice(line.as_bytes());
}
buf
}
// 1. 空数据 → 0 行
#[test]
// 测试:空数据应该有 0 行。
fn test_empty_data() {
// b"" 是一个空的字节字符串(长度为 0 的 &[u8])。
let idx = LineIndex::from_bytes(b"");
assert_eq!(idx.line_count(), 0);
}
// 2. 单行无换行符
#[test]
// 测试:单行内容,末尾没有换行符。
fn test_single_line_no_newline() {
// b"hello" 是一个字节字符串字面量,类型为 &[u8; 5]。
let data = b"hello";
let idx = LineIndex::from_bytes(data);
// 只有 1 行。
assert_eq!(idx.line_count(), 1);
// 第 0 行内容是 "hello"。
assert_eq!(idx.get_line(data, 0), Some("hello"));
}
// 3. 单行带换行符
#[test]
// 测试:单行内容,末尾有换行符。
fn test_single_line_with_newline() {
let data = b"hello\n";
let idx = LineIndex::from_bytes(data);
// 即使末尾有 \n仍然只算 1 行(因为 \n 后面没有内容)。
assert_eq!(idx.line_count(), 1);
// 返回内容时不包含末尾的 \n。
assert_eq!(idx.get_line(data, 0), Some("hello"));
}
// 4. 多行内容
#[test]
// 测试:多行内容。
fn test_multi_line() {
let data = b"aaa\nbbb\nccc";
let idx = LineIndex::from_bytes(data);
@@ -223,69 +220,274 @@ mod tests {
assert_eq!(idx.get_line(data, 2), Some("ccc"));
}
// 5. 300 行:验证行数和关键行内容
#[test]
fn test_300_lines() {
let data = make_lines(300);
let idx = LineIndex::from_bytes(&data);
assert_eq!(idx.line_count(), 300);
assert_eq!(idx.get_line(&data, 0), Some("line0"));
assert_eq!(idx.get_line(&data, 1), Some("line1"));
assert_eq!(idx.get_line(&data, 255), Some("line255"));
assert_eq!(idx.get_line(&data, 256), Some("line256"));
assert_eq!(idx.get_line(&data, 257), Some("line257"));
assert_eq!(idx.get_line(&data, 299), Some("line299"));
}
// 6. 512 行跨块验证
#[test]
fn test_512_lines_cross_block() {
let data = make_lines(512);
let idx = LineIndex::from_bytes(&data);
assert_eq!(idx.line_count(), 512);
assert_eq!(idx.get_line(&data, 511), Some("line511"));
// 跨块边界之后不存在第 512 行
assert_eq!(idx.get_line(&data, 512), None);
}
// 7. 恰好 256 行 + 尾部换行
#[test]
fn test_exactly_256_lines_trailing_newline() {
let data = make_lines(256);
let idx = LineIndex::from_bytes(&data);
assert_eq!(idx.line_count(), 256);
// sampled_offsets: [0](第 0 行trailing pop 移除了第 256 行的采样
assert_eq!(idx.sampled_offsets.len(), 1);
}
// 8. 恰好 512 行 + 尾部换行
#[test]
fn test_exactly_512_lines_trailing_newline() {
let data = make_lines(512);
let idx = LineIndex::from_bytes(&data);
assert_eq!(idx.line_count(), 512);
// sampled_offsets: [0, 256行的偏移, 512行的偏移被pop]
assert_eq!(idx.sampled_offsets.len(), 2);
}
// 9. 257 行 + 尾部换行pop 不应影响
#[test]
fn test_257_lines_trailing_newline_no_pop() {
let data = make_lines(257);
let idx = LineIndex::from_bytes(&data);
assert_eq!(idx.line_count(), 257);
// sampled_offsets: [0, 第256行偏移]trailing 是第 257 行257 % 256 != 0不 pop
assert_eq!(idx.sampled_offsets.len(), 2);
}
// 10. sampled_offsets 数量一致性
#[test]
fn test_sampled_offsets_consistency() {
// 对于不同的总行数,验证 sampled_offsets 数量等于 div_ceil(total_lines, BLOCK_SIZE)
for &n in &[1usize, 100, 256, 257, 300, 512, 513, 1000] {
let data = make_lines(n);
let idx = LineIndex::from_bytes(&data);
let expected = (idx.total_lines as usize + BLOCK_SIZE - 1) / BLOCK_SIZE;
assert_eq!(
idx.sampled_offsets.len(),
expected,
"n={}: expected {} sampled_offsets, got {}",
n,
expected,
idx.sampled_offsets.len()
);
}
}
// 11. CRLF 换行符
#[test]
// 测试Windows 风格的 CRLF 换行符(\r\n
fn test_crlf_endings() {
// b"hello\r\nworld\r\n" — 每行末尾是 \r\n回车+换行)。
let data = b"hello\r\nworld\r\n";
let idx = LineIndex::from_bytes(data);
// 2 行(末尾 \n 后没有内容,所以不算第 3 行)。
assert_eq!(idx.line_count(), 2);
// get_line 会自动去除 \r 和 \n所以内容是干净的。
assert_eq!(idx.get_line(data, 0), Some("hello"));
assert_eq!(idx.get_line(data, 1), Some("world"));
}
// 12. 只有一个换行符
#[test]
// 测试:文件内容只是一个换行符。
fn test_only_newline() {
let data = b"\n";
let idx = LineIndex::from_bytes(data);
// 一个 \n 算作 1 行(内容为空字符串)。
assert_eq!(idx.line_count(), 1);
// 第 0 行内容为空字符串 ""。
assert_eq!(idx.get_line(data, 0), Some(""));
}
// 13. 连续换行符(空行)
#[test]
// 测试:连续的换行符(中间有空行)。
fn test_consecutive_newlines() {
let data = b"a\n\nb";
let idx = LineIndex::from_bytes(data);
// 3 行:'a'、空行、'b'。
assert_eq!(idx.line_count(), 3);
assert_eq!(idx.get_line(data, 0), Some("a"));
// 中间的空行,内容为空字符串 ""。
assert_eq!(idx.get_line(data, 1), Some(""));
assert_eq!(idx.get_line(data, 2), Some("b"));
}
// 14. 行号越界
#[test]
// 测试:两个换行符(两行空行)。
fn test_double_newline() {
let data = b"\n\n";
let idx = LineIndex::from_bytes(data);
// 2 行空行。
assert_eq!(idx.line_count(), 2);
assert_eq!(idx.get_line(data, 0), Some(""));
assert_eq!(idx.get_line(data, 1), Some(""));
}
#[test]
// 测试:行号越界时应该返回 None。
fn test_out_of_bounds() {
let data = b"hello";
let idx = LineIndex::from_bytes(data);
// 只有 1 行,请求第 999 行应该返回 None。
assert_eq!(idx.get_line(data, 999), None);
assert_eq!(idx.get_line(data, 9999), None);
}
// 15. 257 行逐行验证
#[test]
fn test_257_lines_all_lines_verified() {
let data = make_lines(257);
let idx = LineIndex::from_bytes(&data);
assert_eq!(idx.line_count(), 257);
for i in 0..257 {
assert_eq!(
idx.get_line(&data, i),
Some(Box::leak(format!("line{}", i).into_boxed_str()) as &str),
"line {} mismatch",
i
);
}
}
// 16. 跨块第一行(最常见的失败点)
#[test]
fn test_cross_block_first_line() {
let data = make_lines(300);
let idx = LineIndex::from_bytes(&data);
assert_eq!(idx.get_line(&data, 256), Some("line256"));
}
// 17. 混合换行符
#[test]
fn test_mixed_newlines() {
let data = b"hello\r\nworld\nfoo";
let idx = LineIndex::from_bytes(data);
assert_eq!(idx.line_count(), 3);
assert_eq!(idx.get_line(data, 0), Some("hello"));
assert_eq!(idx.get_line(data, 1), Some("world"));
assert_eq!(idx.get_line(data, 2), Some("foo"));
}
// 18. 裸 \r 不算换行符
#[test]
fn test_bare_cr_not_newline() {
let data = b"hello\rworld";
let idx = LineIndex::from_bytes(data);
assert_eq!(idx.line_count(), 1);
// \r 保留在内容中(只去除末尾的 \r
assert_eq!(idx.get_line(data, 0), Some("hello\rworld"));
}
// 19. 短行验证偏移
#[test]
fn test_short_lines() {
let data = b"a\nb\nc";
let idx = LineIndex::from_bytes(data);
assert_eq!(idx.line_count(), 3);
assert_eq!(idx.get_line(data, 0), Some("a"));
assert_eq!(idx.get_line(data, 1), Some("b"));
assert_eq!(idx.get_line(data, 2), Some("c"));
// sampled_offsets 应该只有一个 [0]
assert_eq!(idx.sampled_offsets.len(), 1);
assert_eq!(idx.sampled_offsets[0], 0);
}
// 20. idx 等于 total_lines 应返回 None
#[test]
fn test_idx_equals_total_lines_returns_none() {
let data = b"aaa\nbbb\nccc";
let idx = LineIndex::from_bytes(data);
assert_eq!(idx.line_count(), 3);
assert_eq!(idx.get_line(data, 3), None);
}
// ─── from_reader 测试 ──────────────────────────────────────────────────
use std::io::BufReader;
fn make_reader(data: &[u8]) -> BufReader<std::io::Cursor<&[u8]>> {
BufReader::new(std::io::Cursor::new(data))
}
#[test]
// 测试:空文件的行数和 get_line 都应正确处理。
fn test_empty_file_line_count_and_get_line() {
let idx = LineIndex::from_bytes(b"");
fn test_from_reader_empty() {
let mut reader = make_reader(b"");
let idx = LineIndex::from_reader(&mut reader).unwrap();
assert_eq!(idx.line_count(), 0);
// 空文件,请求第 0 行也应该返回 None因为没有行
assert_eq!(idx.get_line(b"", 0), None);
assert_eq!(idx.sampled_offsets.len(), 0);
}
#[test]
fn test_from_reader_single_line_no_newline() {
let data = b"hello";
let mut reader = make_reader(data);
let idx = LineIndex::from_reader(&mut reader).unwrap();
assert_eq!(idx.line_count(), 1);
assert_eq!(idx.get_line(data, 0), Some("hello"));
}
#[test]
fn test_from_reader_single_line_with_newline() {
let data = b"hello\n";
let mut reader = make_reader(data);
let idx = LineIndex::from_reader(&mut reader).unwrap();
assert_eq!(idx.line_count(), 1);
assert_eq!(idx.get_line(data, 0), Some("hello"));
}
#[test]
fn test_from_reader_matches_from_bytes() {
let test_cases: Vec<&[u8]> = vec![
b"",
b"hello",
b"hello\n",
b"a\nb\nc",
b"\n",
b"a\n\nb",
b"hello\r\nworld\r\n",
b"hello\r\nworld\nfoo",
b"hello\rworld",
];
let lines_300 = make_lines(300);
let lines_257 = make_lines(257);
let lines_512 = make_lines(512);
let mut all_cases: Vec<Vec<u8>> = test_cases.into_iter().map(|s| s.to_vec()).collect();
all_cases.push(lines_300);
all_cases.push(lines_257);
all_cases.push(lines_512);
for (i, data) in all_cases.iter().enumerate() {
let from_bytes_idx = LineIndex::from_bytes(data);
let mut reader = make_reader(data);
let from_reader_idx = LineIndex::from_reader(&mut reader).unwrap();
assert_eq!(
from_bytes_idx.total_lines, from_reader_idx.total_lines,
"case {}: total_lines mismatch (from_bytes={}, from_reader={})",
i, from_bytes_idx.total_lines, from_reader_idx.total_lines
);
assert_eq!(
from_bytes_idx.sampled_offsets, from_reader_idx.sampled_offsets,
"case {}: sampled_offsets mismatch",
i
);
assert_eq!(
from_bytes_idx.has_trailing_newline, from_reader_idx.has_trailing_newline,
"case {}: has_trailing_newline mismatch",
i
);
}
}
#[test]
fn test_from_reader_300_lines_content() {
let data = make_lines(300);
let mut reader = make_reader(&data);
let idx = LineIndex::from_reader(&mut reader).unwrap();
assert_eq!(idx.line_count(), 300);
assert_eq!(idx.get_line(&data, 0), Some("line0"));
assert_eq!(idx.get_line(&data, 255), Some("line255"));
assert_eq!(idx.get_line(&data, 256), Some("line256"));
assert_eq!(idx.get_line(&data, 299), Some("line299"));
}
}