use std::io::{self, Read, Seek, SeekFrom};
use flate2::read::GzDecoder;
use crate::error::FetchError;
use crate::http_range::{HttpRangeReader, MAX_RANGE_REQUESTS, MAX_TRANSFER_BUDGET};
use crate::inspect::is_supported_tensor_file;
const TAIL_SCAN_CHUNK: u64 = 4 * 1024;
const READ_CHUNK: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PeekUnit {
Lines,
Bytes,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum PeekMode {
Cat,
Head {
count: u64,
unit: PeekUnit,
},
Tail {
count: u64,
unit: PeekUnit,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PeekOptions {
pub mode: PeekMode,
pub gunzip: bool,
pub max_bytes: u64,
pub filename: String,
}
impl PeekOptions {
#[must_use]
pub const fn new(mode: PeekMode, gunzip: bool, max_bytes: u64, filename: String) -> Self {
Self {
mode,
gunzip,
max_bytes,
filename,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PeekOutcome {
pub content: Vec<u8>,
pub truncated: Option<String>,
}
pub fn resolve_mode(
head: Option<u64>,
tail: Option<u64>,
bytes: bool,
) -> Result<PeekMode, FetchError> {
let unit = if bytes {
PeekUnit::Bytes
} else {
PeekUnit::Lines
};
match (head, tail) {
(Some(_), Some(_)) => Err(FetchError::InvalidArgument(
"--head and --tail are mutually exclusive".to_owned(),
)),
(Some(0), None) | (None, Some(0)) => Err(FetchError::InvalidArgument(
"--head/--tail count must be at least 1".to_owned(),
)),
(Some(count), None) => Ok(PeekMode::Head { count, unit }),
(None, Some(count)) => Ok(PeekMode::Tail { count, unit }),
(None, None) if bytes => Err(FetchError::InvalidArgument(
"--bytes requires --head or --tail".to_owned(),
)),
(None, None) => Ok(PeekMode::Cat),
}
}
#[must_use]
pub fn resolve_gunzip(gunzip: bool, no_gunzip: bool, filename: &str) -> bool {
if no_gunzip {
return false;
}
gunzip || filename.to_ascii_lowercase().ends_with(".gz")
}
pub fn validate_resolved(options: &PeekOptions, total_size: u64) -> Result<(), FetchError> {
if options.gunzip && matches!(options.mode, PeekMode::Tail { .. }) {
return Err(FetchError::InvalidArgument(format!(
"--gunzip does not support --tail for {} (gzip is sequential); \
run `hf-fm peek {} --gunzip --max <SIZE>` and pipe the output through `tail` instead",
options.filename, options.filename
)));
}
if let PeekMode::Tail {
count,
unit: PeekUnit::Bytes,
} = options.mode
&& count > options.max_bytes
{
return Err(FetchError::InvalidArgument(format!(
"--tail {count} bytes for {} exceeds --max {} (raise --max to read more)",
options.filename,
format_bytes_approx(options.max_bytes)
)));
}
if !options.gunzip && matches!(options.mode, PeekMode::Cat) && total_size > options.max_bytes {
let hint = if is_supported_tensor_file(options.filename.as_str()) {
"use `hf-fm inspect` for tensor files"
} else {
"pass --max <SIZE> to read more of a large text file"
};
return Err(FetchError::InvalidArgument(format!(
"{} is {} (exceeds --max {}); {hint}",
options.filename,
format_bytes_approx(total_size),
format_bytes_approx(options.max_bytes)
)));
}
Ok(())
}
pub fn stream_peek<R: Read + Seek>(
reader: &mut R,
options: &PeekOptions,
) -> io::Result<PeekOutcome> {
match options.mode {
PeekMode::Cat => stream_cat(reader, options),
PeekMode::Head { count, unit } => stream_head(reader, options, count, unit),
PeekMode::Tail {
count,
unit: PeekUnit::Bytes,
} => stream_tail_bytes(reader, count),
PeekMode::Tail {
count,
unit: PeekUnit::Lines,
} => stream_tail_lines(reader, options.max_bytes, count),
}
}
fn stream_cat<R: Read + Seek>(reader: &mut R, options: &PeekOptions) -> io::Result<PeekOutcome> {
if options.gunzip {
let mut decoder = GzDecoder::new(&mut *reader);
let limit = options.max_bytes.saturating_add(1);
let mut buf = Vec::new();
let mut chunk = vec![0u8; READ_CHUNK];
loop {
#[allow(clippy::as_conversions)]
if buf.len() as u64 >= limit {
break;
}
let n = decoder.read(&mut chunk)?;
if n == 0 {
break;
}
let Some(piece) = chunk.get(..n) else {
break; };
buf.extend_from_slice(piece);
}
#[allow(clippy::as_conversions)]
let written = buf.len() as u64;
if written > options.max_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{} decompressed content exceeds --max {} (raise --max to read more)",
options.filename,
format_bytes_approx(options.max_bytes)
),
));
}
Ok(PeekOutcome {
content: buf,
truncated: None,
})
} else {
let mut content = Vec::new();
reader.read_to_end(&mut content)?;
Ok(PeekOutcome {
content,
truncated: None,
})
}
}
fn stream_head<R: Read + Seek>(
reader: &mut R,
options: &PeekOptions,
count: u64,
unit: PeekUnit,
) -> io::Result<PeekOutcome> {
let mut src: Box<dyn Read + '_> = if options.gunzip {
Box::new(GzDecoder::new(&mut *reader))
} else {
Box::new(&mut *reader)
};
let (content, stop) = bounded_copy(&mut src, count, unit, options.max_bytes)?;
let truncated = matches!(stop, StopReason::CapExceeded).then(|| {
format!(
"truncated: --head {count} {} for {} not fully read within --max {} \
(raise --max to read more)",
unit_label(unit),
options.filename,
format_bytes_approx(options.max_bytes)
)
});
Ok(PeekOutcome { content, truncated })
}
fn stream_tail_bytes<R: Read + Seek>(reader: &mut R, count: u64) -> io::Result<PeekOutcome> {
let end = reader.seek(SeekFrom::End(0))?;
let start = end.saturating_sub(count);
reader.seek(SeekFrom::Start(start))?;
let mut content = Vec::new();
reader.read_to_end(&mut content)?;
Ok(PeekOutcome {
content,
truncated: None,
})
}
fn stream_tail_lines<R: Read + Seek>(
reader: &mut R,
max_bytes: u64,
count: u64,
) -> io::Result<PeekOutcome> {
let end = reader.seek(SeekFrom::End(0))?;
let mut scan = TAIL_SCAN_CHUNK.min(end).min(max_bytes);
loop {
let start = end.saturating_sub(scan);
reader.seek(SeekFrom::Start(start))?;
let read_len = end.saturating_sub(start);
let mut buf = vec![0u8; usize::try_from(read_len).unwrap_or(usize::MAX)];
reader.read_exact(&mut buf)?;
#[allow(clippy::naive_bytecount)]
let newline_count = buf.iter().filter(|&&b| b == b'\n').count();
let newline_count_u64 = u64::try_from(newline_count).unwrap_or(u64::MAX);
let hit_start = start == 0;
let hit_cap = scan >= max_bytes;
let confident = newline_count_u64 > count || hit_start;
if confident || hit_cap {
let content = last_n_lines(&buf, count);
let truncated = (!confident).then(|| {
if newline_count_u64 < count {
format!(
"truncated: only {newline_count_u64} of {count} requested lines \
available within --max {} (raise --max to read more)",
format_bytes_approx(max_bytes)
)
} else {
format!(
"truncated: found {count} requested lines but --max {} was \
reached before confirming the start of the earliest one \
(raise --max to read more)",
format_bytes_approx(max_bytes)
)
}
});
return Ok(PeekOutcome { content, truncated });
}
scan = scan.saturating_mul(2).min(end).min(max_bytes);
}
}
fn last_n_lines(buf: &[u8], count: u64) -> Vec<u8> {
let positions: Vec<usize> = buf
.iter()
.enumerate()
.filter_map(|(i, &b)| (b == b'\n').then_some(i))
.collect();
let m = positions.len();
#[allow(clippy::as_conversions)]
let count_usize = usize::try_from(count).unwrap_or(usize::MAX);
if m > count_usize && count_usize > 0 {
let boundary_idx = m - count_usize - 1;
let cut = positions
.get(boundary_idx)
.map_or(0, |p| p.saturating_add(1));
buf.get(cut..).unwrap_or(buf).to_vec()
} else {
buf.to_vec()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StopReason {
CountSatisfied,
Eof,
CapExceeded,
}
fn bounded_copy(
src: &mut impl Read,
count: u64,
unit: PeekUnit,
cap: u64,
) -> io::Result<(Vec<u8>, StopReason)> {
let mut content = Vec::new();
let mut lines_seen: u64 = 0;
let mut buf = vec![0u8; READ_CHUNK];
loop {
#[allow(clippy::as_conversions)]
let written = content.len() as u64;
if written >= cap {
return Ok((content, StopReason::CapExceeded));
}
#[allow(clippy::as_conversions)]
let read_chunk_u64 = READ_CHUNK as u64;
let remaining_cap =
usize::try_from(cap.saturating_sub(written).min(read_chunk_u64)).unwrap_or(READ_CHUNK);
let Some(target) = buf.get_mut(..remaining_cap) else {
return Ok((content, StopReason::CapExceeded)); };
let n = src.read(target)?;
if n == 0 {
return Ok((content, StopReason::Eof));
}
let Some(chunk) = buf.get(..n) else {
return Ok((content, StopReason::CapExceeded)); };
match unit {
PeekUnit::Bytes => {
#[allow(clippy::as_conversions)]
let remaining = usize::try_from(count.saturating_sub(written)).unwrap_or(n);
let take = remaining.min(n);
let Some(piece) = chunk.get(..take) else {
return Ok((content, StopReason::CapExceeded)); };
content.extend_from_slice(piece);
#[allow(clippy::as_conversions)]
let new_written = written.saturating_add(take as u64);
if new_written >= count {
return Ok((content, StopReason::CountSatisfied));
}
}
PeekUnit::Lines => {
let mut remaining = chunk;
loop {
if let Some(nl) = remaining.iter().position(|&b| b == b'\n') {
let Some((line, rest)) = remaining.split_at_checked(nl + 1) else {
break; };
content.extend_from_slice(line);
lines_seen += 1;
if lines_seen >= count {
return Ok((content, StopReason::CountSatisfied));
}
remaining = rest;
} else {
content.extend_from_slice(remaining);
break;
}
}
}
}
}
}
const fn unit_label(unit: PeekUnit) -> &'static str {
match unit {
PeekUnit::Lines => "lines",
PeekUnit::Bytes => "bytes",
}
}
fn format_bytes_approx(bytes: u64) -> String {
const KIB: u64 = 1024;
const MIB: u64 = 1024 * 1024;
const GIB: u64 = 1024 * 1024 * 1024;
const TIB: u64 = 1024 * GIB;
if bytes >= 1000 * GIB {
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
let val = bytes as f64 / TIB as f64;
format!("{val:.2} TiB")
} else if bytes >= 1000 * MIB {
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
let val = bytes as f64 / GIB as f64;
format!("{val:.2} GiB")
} else if bytes >= MIB {
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
let val = bytes as f64 / MIB as f64;
format!("{val:.2} MiB")
} else if bytes >= KIB {
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
let val = bytes as f64 / KIB as f64;
format!("{val:.1} KiB")
} else {
format!("{bytes} B")
}
}
fn transport_limits(max_bytes: u64) -> (u32, u64) {
let max_transfer_bytes = max_bytes.saturating_mul(2).max(MAX_TRANSFER_BUDGET);
#[allow(clippy::as_conversions)]
let read_chunk_u64 = READ_CHUNK as u64;
let by_chunk = (max_bytes / read_chunk_u64).saturating_add(32);
let max_requests = u32::try_from(by_chunk)
.unwrap_or(u32::MAX)
.max(MAX_RANGE_REQUESTS);
(max_requests, max_transfer_bytes)
}
pub async fn peek(
repo_id: &str,
filename: &str,
token: Option<&str>,
revision: Option<&str>,
options: PeekOptions,
) -> Result<PeekOutcome, FetchError> {
let (max_requests, max_transfer_bytes) = transport_limits(options.max_bytes);
let mut reader = HttpRangeReader::open_with_limits(
repo_id,
revision,
filename,
token,
max_requests,
max_transfer_bytes,
)
.await?;
validate_resolved(&options, reader.total_size())?;
let (result, transport_error) = tokio::task::spawn_blocking(move || {
let outcome = stream_peek(&mut reader, &options);
(outcome, reader.take_last_error())
})
.await
.map_err(|e| FetchError::Http(format!("failed to join peek task: {e}")))?;
match result {
Ok(outcome) => Ok(outcome),
Err(io_err) if io_err.kind() == io::ErrorKind::InvalidData => {
Err(FetchError::InvalidArgument(io_err.to_string()))
}
Err(io_err) => Err(transport_error
.unwrap_or_else(|| FetchError::Http(format!("failed to peek {filename}: {io_err}")))),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use std::io::Cursor;
use super::*;
fn opts(mode: PeekMode, gunzip: bool, max_bytes: u64) -> PeekOptions {
PeekOptions::new(mode, gunzip, max_bytes, "test.txt".to_owned())
}
fn gzip(data: &[u8]) -> Vec<u8> {
use std::io::Write as _;
let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
enc.write_all(data).unwrap();
enc.finish().unwrap()
}
#[test]
fn format_bytes_approx_matches_format_size_test_cases() {
assert_eq!(format_bytes_approx(0), "0 B");
assert_eq!(format_bytes_approx(1), "1 B");
assert_eq!(format_bytes_approx(1023), "1023 B");
assert_eq!(format_bytes_approx(1024), "1.0 KiB");
assert_eq!(format_bytes_approx(1536), "1.5 KiB");
assert_eq!(format_bytes_approx(1024 * 1024), "1.00 MiB");
assert_eq!(format_bytes_approx(10 * 1024 * 1024), "10.00 MiB");
assert_eq!(format_bytes_approx(999 * 1024 * 1024), "999.00 MiB");
let gib = 1u64 << 30;
assert_eq!(format_bytes_approx(gib), "1.00 GiB");
assert_eq!(format_bytes_approx(999 * gib), "999.00 GiB");
let tib = 1024 * gib;
assert_eq!(format_bytes_approx(tib), "1.00 TiB");
}
#[test]
fn transport_limits_floors_at_the_transport_defaults() {
let (max_requests, max_transfer_bytes) = transport_limits(1024);
assert_eq!(max_requests, MAX_RANGE_REQUESTS);
assert_eq!(max_transfer_bytes, MAX_TRANSFER_BUDGET);
}
#[test]
fn transport_limits_scales_past_the_defaults_for_a_large_max() {
let large = 200 * 1024 * 1024; let (max_requests, max_transfer_bytes) = transport_limits(large);
assert!(
max_transfer_bytes >= 2 * large,
"must cover the doubled-headroom formula, got {max_transfer_bytes}"
);
assert!(
max_requests > MAX_RANGE_REQUESTS,
"a 200 MiB --max needs more than the {MAX_RANGE_REQUESTS}-request \
default at {READ_CHUNK}-byte chunks, got {max_requests}"
);
}
#[test]
fn resolve_mode_covers_the_flag_matrix() {
assert!(matches!(
resolve_mode(None, None, false).unwrap(),
PeekMode::Cat
));
assert!(matches!(
resolve_mode(Some(5), None, false).unwrap(),
PeekMode::Head {
count: 5,
unit: PeekUnit::Lines
}
));
assert!(matches!(
resolve_mode(None, Some(5), true).unwrap(),
PeekMode::Tail {
count: 5,
unit: PeekUnit::Bytes
}
));
assert!(resolve_mode(Some(1), Some(1), false).is_err());
assert!(resolve_mode(Some(0), None, false).is_err());
assert!(resolve_mode(None, None, true).is_err());
}
#[test]
fn resolve_gunzip_prefers_no_gunzip_then_explicit_then_extension() {
assert!(!resolve_gunzip(false, true, "data.json.gz"));
assert!(resolve_gunzip(true, false, "data.json"));
assert!(resolve_gunzip(false, false, "index.JSON.GZ"));
assert!(!resolve_gunzip(false, false, "config.yaml"));
}
#[test]
fn validate_resolved_rejects_oversized_non_gzip_cat_upfront() {
let o = opts(PeekMode::Cat, false, 10);
let err = validate_resolved(&o, 11).unwrap_err();
assert!(matches!(err, FetchError::InvalidArgument(_)));
}
#[test]
fn validate_resolved_names_inspect_for_tensor_files() {
let o = PeekOptions::new(PeekMode::Cat, false, 10, "model.safetensors".to_owned());
let err = validate_resolved(&o, 11).unwrap_err();
assert!(err.to_string().contains("hf-fm inspect"), "{err}");
}
#[test]
fn validate_resolved_rejects_gunzip_with_tail() {
let o = opts(
PeekMode::Tail {
count: 5,
unit: PeekUnit::Lines,
},
true,
1024,
);
let err = validate_resolved(&o, 100).unwrap_err();
assert!(err.to_string().contains("--tail"), "{err}");
}
#[test]
fn validate_resolved_rejects_tail_bytes_over_max() {
let o = opts(
PeekMode::Tail {
count: 100,
unit: PeekUnit::Bytes,
},
false,
10,
);
let err = validate_resolved(&o, 1000).unwrap_err();
assert!(err.to_string().contains("--max"), "{err}");
}
#[test]
fn validate_resolved_allows_cat_under_cap() {
let o = opts(PeekMode::Cat, false, 10);
validate_resolved(&o, 10).unwrap();
}
#[test]
fn cat_streams_everything_under_cap() {
let data = b"hello world\n".to_vec();
let mut r = Cursor::new(data.clone());
let o = opts(PeekMode::Cat, false, 1024);
let out = stream_peek(&mut r, &o).unwrap();
assert_eq!(out.content, data);
assert!(out.truncated.is_none());
}
#[test]
fn cat_gunzip_round_trips_and_rejects_over_cap() {
let data = b"line one\nline two\nline three\n".to_vec();
let compressed = gzip(&data);
let mut r = Cursor::new(compressed.clone());
let o = opts(PeekMode::Cat, true, 1024);
let out = stream_peek(&mut r, &o).unwrap();
assert_eq!(out.content, data);
let mut r2 = Cursor::new(compressed);
let tiny = opts(PeekMode::Cat, true, 4);
let err = stream_peek(&mut r2, &tiny).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn head_lines_stops_at_the_nth_newline() {
let data = b"a\nb\nc\nd\n".to_vec();
let mut r = Cursor::new(data);
let o = opts(
PeekMode::Head {
count: 2,
unit: PeekUnit::Lines,
},
false,
1024,
);
let out = stream_peek(&mut r, &o).unwrap();
assert_eq!(out.content, b"a\nb\n");
assert!(out.truncated.is_none());
}
#[test]
fn head_lines_short_file_is_not_truncated() {
let data = b"only one line\n".to_vec();
let mut r = Cursor::new(data.clone());
let o = opts(
PeekMode::Head {
count: 5,
unit: PeekUnit::Lines,
},
false,
1024,
);
let out = stream_peek(&mut r, &o).unwrap();
assert_eq!(out.content, data);
assert!(
out.truncated.is_none(),
"short file is not a --max truncation"
);
}
#[test]
fn head_bytes_exact_count() {
let data = b"abcdefgh".to_vec();
let mut r = Cursor::new(data);
let o = opts(
PeekMode::Head {
count: 3,
unit: PeekUnit::Bytes,
},
false,
1024,
);
let out = stream_peek(&mut r, &o).unwrap();
assert_eq!(out.content, b"abc");
}
#[test]
fn head_cap_exceeded_before_count_is_truncated() {
let data = b"no_newlines_at_all_here".to_vec();
let mut r = Cursor::new(data);
let o = opts(
PeekMode::Head {
count: 1,
unit: PeekUnit::Lines,
},
false,
4,
);
let out = stream_peek(&mut r, &o).unwrap();
assert_eq!(out.content.len(), 4);
assert!(out.truncated.is_some());
}
#[test]
fn tail_bytes_reads_exactly_the_last_n() {
let data = b"0123456789".to_vec();
let mut r = Cursor::new(data);
let out = stream_tail_bytes(&mut r, 4).unwrap();
assert_eq!(out.content, b"6789");
}
#[test]
fn tail_bytes_clamps_when_file_is_shorter_than_requested() {
let data = b"hi".to_vec();
let mut r = Cursor::new(data.clone());
let out = stream_tail_bytes(&mut r, 100).unwrap();
assert_eq!(out.content, data);
}
#[test]
fn tail_lines_returns_the_last_n_lines() {
let data = b"a\nb\nc\nd\ne\n".to_vec();
let mut r = Cursor::new(data);
let out = stream_tail_lines(&mut r, 1024, 2).unwrap();
assert_eq!(out.content, b"d\ne\n");
assert!(out.truncated.is_none());
}
#[test]
fn tail_lines_exact_newline_count_at_window_edge_does_not_splice_a_partial_line() {
let line0 = format!("{}\n", "A".repeat(10_000)); let line1 = format!("{}\n", "Q".repeat(5_000)); let line2 = "last\n"; let data = format!("{line0}{line1}{line2}").into_bytes();
let mut r = Cursor::new(data);
let out = stream_tail_lines(&mut r, 1024 * 1024, 2).unwrap();
assert_eq!(
out.content,
format!("{line1}{line2}").into_bytes(),
"must be exactly the last two complete lines, not a partial-line splice"
);
assert!(
out.truncated.is_none(),
"the scan grew far enough to confirm the boundary; not a --max truncation"
);
}
#[test]
fn tail_lines_grows_the_scan_window_past_one_chunk() {
let filler = "x".repeat(6000);
let data = format!("{filler}\nlast one\nlast two\n").into_bytes();
let mut r = Cursor::new(data);
let out = stream_tail_lines(&mut r, 1024 * 1024, 2).unwrap();
assert_eq!(out.content, b"last one\nlast two\n");
}
#[test]
fn tail_lines_short_file_returns_everything_untruncated() {
let data = b"only\ntwo\n".to_vec();
let mut r = Cursor::new(data.clone());
let out = stream_tail_lines(&mut r, 1024, 10).unwrap();
assert_eq!(out.content, data);
assert!(out.truncated.is_none());
}
#[test]
fn tail_lines_bounded_by_max_is_truncated() {
let filler = "y".repeat(6000);
let data = format!("{filler}\nlast\n").into_bytes();
let mut r = Cursor::new(data);
let out = stream_tail_lines(&mut r, 128, 5).unwrap();
assert!(out.truncated.is_some());
}
#[test]
fn tail_lines_empty_file() {
let mut r = Cursor::new(Vec::<u8>::new());
let out = stream_tail_lines(&mut r, 1024, 3).unwrap();
assert!(out.content.is_empty());
assert!(out.truncated.is_none());
}
#[test]
fn last_n_lines_pure_helper() {
assert_eq!(last_n_lines(b"a\nb\nc\n", 2), b"b\nc\n");
assert_eq!(last_n_lines(b"a\nb\nc\n", 10), b"a\nb\nc\n");
assert_eq!(last_n_lines(b"no newline here", 1), b"no newline here");
}
#[test]
fn head_over_range_reader_reads_only_the_needed_window() {
use crate::http_range::{RangeFetcher, RangeReader};
struct InMemory {
data: Vec<u8>,
}
impl RangeFetcher for InMemory {
fn fetch(&mut self, start: u64, end_inclusive: u64) -> Result<Vec<u8>, FetchError> {
let s = usize::try_from(start).unwrap();
let e = usize::try_from(end_inclusive).unwrap();
self.data
.get(s..=e)
.map(<[u8]>::to_vec)
.ok_or_else(|| FetchError::Http("bad range".to_owned()))
}
fn total_size(&self) -> u64 {
u64::try_from(self.data.len()).unwrap()
}
}
let mut body = "first\nsecond\n".as_bytes().to_vec();
body.extend(std::iter::repeat_n(b'z', 200 * 1024)); let mut reader = RangeReader::new(InMemory { data: body });
let o = opts(
PeekMode::Head {
count: 2,
unit: PeekUnit::Lines,
},
false,
1024,
);
let out = stream_peek(&mut reader, &o).unwrap();
assert_eq!(out.content, b"first\nsecond\n");
assert!(
reader.stats().bytes_fetched < 200 * 1024,
"head must not fetch the large unused tail"
);
}
}