use std::io::BufRead;
use anyhow::{Result, bail};
use tokio::io::{AsyncRead, AsyncReadExt};
fn ensure_within_max(buf: &str, max_bytes: usize) -> Result<()> {
if buf.len() > max_bytes {
bail!(
"stdin input exceeds {max_bytes} bytes; \
increase with --max-value-bytes or SALUSC_STORE_MAX_VALUE_BYTES"
);
}
Ok(())
}
fn strip_trailing_newline(buf: &mut String) {
if buf.ends_with('\n') {
let _ = buf.pop();
if buf.ends_with('\r') {
let _ = buf.pop();
}
}
}
pub(crate) fn read_interactive_value<R: BufRead>(reader: R, max_bytes: usize) -> Result<String> {
let mut buf = String::new();
let _ = reader
.take((max_bytes as u64).saturating_add(1))
.read_line(&mut buf)?;
ensure_within_max(&buf, max_bytes)?;
strip_trailing_newline(&mut buf);
Ok(buf)
}
pub(crate) async fn read_piped_value<R: AsyncRead + Unpin>(
reader: R,
max_bytes: usize,
) -> Result<String> {
let mut buf = String::new();
let _ = reader
.take((max_bytes as u64).saturating_add(1))
.read_to_string(&mut buf)
.await?;
ensure_within_max(&buf, max_bytes)?;
strip_trailing_newline(&mut buf);
Ok(buf)
}
#[cfg(test)]
mod test {
use anyhow::{Result, bail};
use super::{read_interactive_value, read_piped_value, strip_trailing_newline};
#[test]
fn strip_trailing_newline_handles_lf() {
let mut buf = String::from("abc\n");
strip_trailing_newline(&mut buf);
assert_eq!(buf, "abc");
}
#[test]
fn strip_trailing_newline_handles_crlf() {
let mut buf = String::from("abc\r\n");
strip_trailing_newline(&mut buf);
assert_eq!(buf, "abc");
}
#[test]
fn strip_trailing_newline_handles_none() {
let mut buf = String::from("abc");
strip_trailing_newline(&mut buf);
assert_eq!(buf, "abc");
}
#[test]
fn read_interactive_value_strips_lf() -> Result<()> {
let value = read_interactive_value(b"hello\n".as_slice(), 1024)?;
assert_eq!(value, "hello");
Ok(())
}
#[test]
fn read_interactive_value_strips_crlf() -> Result<()> {
let value = read_interactive_value(b"hello\r\n".as_slice(), 1024)?;
assert_eq!(value, "hello");
Ok(())
}
#[test]
fn read_interactive_value_rejects_oversized_input() -> Result<()> {
match read_interactive_value(b"toolong\n".as_slice(), 3) {
Ok(value) => bail!("expected an error, got Ok({value:?})"),
Err(error) => {
let message = error.to_string();
if !message.contains("exceeds") {
bail!("unexpected error message: {message}");
}
}
}
Ok(())
}
#[tokio::test]
async fn read_piped_value_supports_multiline_eof_terminated_input() -> Result<()> {
let value = read_piped_value(b"line one\nline two".as_slice(), 1024).await?;
assert_eq!(value, "line one\nline two");
Ok(())
}
#[tokio::test]
async fn read_piped_value_strips_trailing_newline() -> Result<()> {
let value = read_piped_value(b"secret\n".as_slice(), 1024).await?;
assert_eq!(value, "secret");
Ok(())
}
#[tokio::test]
async fn read_piped_value_rejects_oversized_input() -> Result<()> {
match read_piped_value(b"toolong".as_slice(), 3).await {
Ok(value) => bail!("expected an error, got Ok({value:?})"),
Err(error) => {
let message = error.to_string();
if !message.contains("exceeds") {
bail!("unexpected error message: {message}");
}
}
}
Ok(())
}
}