mod range;
use serde::Deserialize;
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::PathBuf;
use thiserror::Error;
pub const MAX_BYTES: u64 = 1024 * 1024;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Input {
path: PathBuf,
offset: Option<u64>,
limit: Option<u64>,
}
const FIRST_LINE: u64 = 1;
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid input JSON: {0}")]
InvalidJson(#[source] serde_json::Error),
#[error("read input from stdin: {0}")]
StdinRead(#[source] io::Error),
#[error("open {path}: {source}", path = path.display())]
Open {
path: PathBuf,
#[source]
source: io::Error,
},
#[error(
"file {path} is {size} bytes (cap {cap}); use a streaming tool",
path = path.display()
)]
TooLarge { path: PathBuf, cap: u64, size: u64 },
#[error("read {path}: {source}", path = path.display())]
Read {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("{field} must be 1 or more (lines count from 1); got 0")]
Range { field: &'static str },
#[error("write to stdout: {0}")]
Write(#[source] io::Error),
}
pub fn run<R: Read, W: Write, E: Write>(
stdin: &mut R,
stdout: &mut W,
stderr: &mut E,
) -> Result<(), Error> {
let mut buf = Vec::new();
stdin.read_to_end(&mut buf).map_err(Error::StdinRead)?;
let input: Input = serde_json::from_slice(&buf).map_err(Error::InvalidJson)?;
let offset = input.offset.unwrap_or(FIRST_LINE);
if offset == 0 {
return Err(Error::Range { field: "offset" });
}
if input.limit == Some(0) {
return Err(Error::Range { field: "limit" });
}
let path = input.path;
let file = File::open(&path).map_err(|source| Error::Open {
path: path.clone(),
source,
})?;
let mut content = Vec::new();
let mut capped = file.take(MAX_BYTES + 1);
capped
.read_to_end(&mut content)
.map_err(|source| Error::Read {
path: path.clone(),
source,
})?;
let read = content.len() as u64;
if read > MAX_BYTES {
let size = capped
.get_ref()
.metadata()
.map_or(read, |m| m.len().max(read));
let cap = MAX_BYTES;
return Err(Error::TooLarge { path, cap, size });
}
let selected = range::select(&content, offset, input.limit);
stdout
.write_all(
content
.get(selected.start..selected.end)
.unwrap_or_default(),
)
.map_err(Error::Write)?;
stderr
.write_all(selected.note().as_bytes())
.map_err(Error::Write)
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_range;