You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用tokio-tar异步读取tar.gz条目时,读取字节数>0但缓冲区为空

问题:tokio-tar读取归档条目时缓冲区为空但字节数正常

我正在编写基于Tokio的异步程序,支持透明读取普通文件、gzip压缩文件和gzip压缩tar归档文件。前两种文件读取正常,但用tokio-tar读取tar归档文件时遇到问题:读取的字节数大于0,但缓冲区始终为空。

复现代码

#![feature(let_chains)]
use std::path::Path;

use async_compression::tokio::bufread::GzipDecoder;
use futures::StreamExt;
use tokio::{
    fs::File,
    io::{AsyncReadExt, BufReader},
};
use tokio_tar::{Archive, Entry};

#[derive(Debug)]
pub enum Handle {
    File(BufReader<File>),
    Gz(GzipDecoder<BufReader<File>>),
    TarGz(Entry<Archive<GzipDecoder<BufReader<File>>>>),
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // prepare the reader
    let f = File::open("./archive.tar.gz").await?;
    let gz = GzipDecoder::new(BufReader::new(f));
    let mut archive = Archive::new(gz);

    // the path of the file to read within the archive
    let path_wanted = Path::new("tar/archive.txt");
    let mut entries = archive.entries()?;
    let mut handle: Option<Handle> = None;

    // iterate through the files in the archive until the desired file is found
    while let Some(entry_result) = entries.next().await {
        if let Ok(entry) = entry_result
            && let Ok(path) = entry.path()
            && path == path_wanted
        {
            handle = Some(Handle::TarGz(entry));
        }
    }

    // use the assigned variable to read the archive entry's contents
    if let Some(mut tgz) = handle {
        match tgz {
            Handle::TarGz(ref mut tgz) => {
                let mut vec_buf = vec![0; 10];
                loop {
                    match tgz.take(10).read_to_end(&mut vec_buf).await {
                        // here the actual problem becomes visible
                        // while reading the entry, n is greater than 0 but nothing is written to the buffer
                        Ok(n) if n > 0 => {
                            println!("n: {} - {:?}", n, &vec_buf[..n]);
                        }
                        _ => break,
                    }
                }
            }
            _ => panic!("case not covered"),
        }
    } else {
        println!("path_wanted not found in archive");
    }
    Ok(())
}

归档文件内容

127.0.0.1   test.com
127.0.0.1   test.de

程序输出

tarvester on  master [?] is 📦 v0.1.0 via 🦀 v1.66.0-nightly
❯ cargo run
   Compiling tarvester v0.1.0 (/home/bob/code/gardion/tarvester)
    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/tarvester`
n: 10 - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
n: 10 - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
n: 10 - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
n: 7 - [0, 0, 0, 0, 0, 0, 0]

修改版代码(输出混乱)

#![feature(let_chains)]
use std::path::Path;

use async_compression::tokio::bufread::GzipDecoder;
use futures::StreamExt;
use tokio::{
    fs::File,
    io::{AsyncReadExt, BufReader},
};
use tokio_tar::{Archive, Entry};

#[derive(Debug)]
pub enum Handle {
    File(BufReader<File>),
    Gz(GzipDecoder<BufReader<File>>),
    TarGz(Entry<Archive<GzipDecoder<BufReader<File>>>>),
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let f = File::open("../harvester/archive.tar.gz").await?;
    let gz = GzipDecoder::new(BufReader::new(f));
    let mut archive = Archive::new(gz);

    let path_wanted = Path::new("tar/archive.txt");
    let mut entries = archive.entries()?;
    let mut handle: Option<Handle> = None;
    while let Some(entry_result) = entries.next().await {
        if let Ok(entry) = entry_result
            && let Ok(path) = entry.path()
            && path == path_wanted
        {
            handle = Some(Handle::TarGz(entry));
            // !! the handle assigned to the variable is used right away without leaving the block
            match handle.as_mut().unwrap() {
                Handle::TarGz(tgz) => {
                    let mut vec_buf = vec![0; 10];
                    loop {
                        match tgz.take(10).read_to_end(&mut vec_buf).await {
                            // some messed up output shows up here
                            Ok(n) if n > 0 => {
                                println!("{}", String::from_utf8_lossy(&vec_buf[..]));
                            },
                            _ => break
                        }
                    }
                },
                _ => panic!("case not covered"),
            }
        }
    }
    Ok(())
}

修改版输出

127.0.0.1
127.0.0.1       test.com
1
127.0.0.1       test.com
127.0.0.1       t
127.0.0.1       test.com
127.0.0.1       test.de

问题原因与修复

1. 第一个版本的核心问题:Archive被提前销毁

Entry类型持有Archive的内部引用,当你找到目标条目后继续遍历所有entries直到流结束,archive变量会被自动销毁。后续读取已失效的Entry时,实际操作的是已释放的资源,导致缓冲区全为0。

修复方法:找到目标条目后立即跳出循环,保留Archive的生命周期:

handle = Some(Handle::TarGz(entry));
break; // 关键:停止遍历,避免Archive被销毁

2. 读取方式误用:take()与read_to_end()的错误搭配

  • tgz.take(10)每次调用都会创建新的Take包装器,消耗Entry的读取状态,导致读取异常。
  • read_to_end(&mut vec_buf)会将数据追加到Vec末尾,而你复用固定长度缓冲区的方式不符合该方法的使用逻辑。

正确的读取方式是直接使用read(&mut vec_buf)读取指定长度数据:

let mut vec_buf = vec![0; 10];
loop {
    match tgz.read(&mut vec_buf).await {
        Ok(n) if n > 0 => {
            println!("n: {} - {:?}", n, &vec_buf[..n]);
            println!("content: {}", String::from_utf8_lossy(&vec_buf[..n]));
        }
        Ok(0) => break, // 读取完成
        Err(e) => {
            eprintln!("read error: {}", e);
            break;
        }
    }
}

3. 修改版输出混乱的原因

修改版中每次打印整个vec_buf,而read_to_end会不断向缓冲区追加数据,导致输出包含之前读取的旧内容。正确做法是只打印读取到的n个字节,即&vec_buf[..n]。


内容的提问来源于stack exchange,提问作者bob3000

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 02:30:48