pub(crate) mod address;
mod error;
use super::super::ENV_CONV_BRANCH;
use super::super::ENV_CONV_REPO;
use super::super::ToolOutputRecord;
use super::dispatch::EnvLookup;
use address::Address;
pub use error::Error;
use serde::Deserialize;
use std::io::{Read, Write};
use std::path::Path;
const PAGE_BYTES: usize = 4096;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Input {
address: String,
}
pub fn run<R: Read, W: Write, E: Write>(
stdin: &mut R,
stdout: &mut W,
stderr: &mut E,
env: &dyn EnvLookup,
) -> 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 repo = env
.get(ENV_CONV_REPO)
.ok_or(Error::MissingEnv(ENV_CONV_REPO))?;
let caller = env
.get(ENV_CONV_BRANCH)
.ok_or(Error::MissingEnv(ENV_CONV_BRANCH))?
.to_string_lossy()
.into_owned();
let address = Address::parse(&input.address).ok_or_else(|| Error::Malformed {
given: input.address.clone(),
})?;
if address.agent_id != caller {
return Err(Error::ForeignAgent {
theirs: address.agent_id,
ours: caller,
});
}
let record = Path::new(&repo).join(address.record());
let bytes = std::fs::read(&record).map_err(|source| Error::NoRecord {
record: record.clone(),
source,
})?;
let captured: ToolOutputRecord =
serde_json::from_slice(&bytes).map_err(|source| Error::Unreadable { record, source })?;
let stream = match address.stream.as_str() {
"stderr" => captured.stderr,
_ => captured.stdout,
};
let page = page(stream.as_bytes(), address.offset).ok_or_else(|| Error::PastEnd {
offset: address.offset,
stream: address.stream.clone(),
total: stream.len(),
})?;
stdout.write_all(page).map_err(Error::Write)?;
let end = address.offset.saturating_add(page.len());
stderr
.write_all(banner(&address, end, stream.len()).as_bytes())
.map_err(Error::Write)
}
fn page(stream: &[u8], offset: usize) -> Option<&[u8]> {
let end = offset.saturating_add(PAGE_BYTES).min(stream.len());
stream.get(offset..end)
}
fn banner(address: &Address, end: usize, total: usize) -> String {
let (offset, stream, record) = (address.offset, &address.stream, address.record());
let rest = if end < total {
format!("continue with address {}", address.at(end))
} else {
"end of stream".to_string()
};
format!("read_tool_output: bytes {offset}-{end} of {total} from {record} {stream}; {rest}\n")
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_address;