use std::io::{self, BufRead, IsTerminal};
use std::path::Path;
const MAX_INPUT_SIZE: usize = 100 * 1024 * 1024;
const MAX_LINE_SIZE: usize = 10 * 1024 * 1024;
#[derive(Debug)]
pub enum InputSource {
File(String),
Stdin(String),
}
#[derive(Debug)]
pub enum InputError {
Io(io::Error),
Utf8Error,
EmptyInput,
NoTty,
InputTooLarge(usize),
LineTooLong(usize),
}
impl std::fmt::Display for InputError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InputError::Io(e) => write!(f, "I/O error: {}", e),
InputError::Utf8Error => write!(f, "Invalid UTF-8 in input"),
InputError::EmptyInput => write!(f, "Empty input provided"),
InputError::NoTty => {
write!(f, "No file specified and stdin is not being piped")
}
InputError::InputTooLarge(size) => {
write!(
f,
"Input too large: {} bytes (max {} MB)",
size,
MAX_INPUT_SIZE / (1024 * 1024)
)
}
InputError::LineTooLong(size) => {
write!(
f,
"Line too long: {} bytes (max {} MB)",
size,
MAX_LINE_SIZE / (1024 * 1024)
)
}
}
}
}
impl std::error::Error for InputError {}
impl From<io::Error> for InputError {
fn from(e: io::Error) -> Self {
InputError::Io(e)
}
}
pub fn is_stdin_piped() -> bool {
!io::stdin().is_terminal()
}
pub fn read_stdin() -> Result<String, InputError> {
let stdin = io::stdin();
let mut handle = stdin.lock();
let mut buffer = String::new();
let mut total_size = 0usize;
let mut line_buffer = String::new();
loop {
line_buffer.clear();
let bytes_read = handle.read_line(&mut line_buffer)?;
if bytes_read == 0 {
break;
}
if line_buffer.len() > MAX_LINE_SIZE {
return Err(InputError::LineTooLong(line_buffer.len()));
}
total_size = total_size.saturating_add(bytes_read);
if total_size > MAX_INPUT_SIZE {
return Err(InputError::InputTooLarge(total_size));
}
buffer.push_str(&line_buffer);
}
if buffer.is_empty() {
return Err(InputError::EmptyInput);
}
Ok(buffer)
}
pub fn determine_input_source(file_path: Option<&Path>) -> Result<InputSource, InputError> {
match file_path {
Some(path) if path == Path::new("-") => {
let content = read_stdin()?;
Ok(InputSource::Stdin(content))
}
Some(path) => {
let content = std::fs::read_to_string(path).map_err(InputError::Io)?;
Ok(InputSource::File(content))
}
None if is_stdin_piped() => {
let content = read_stdin()?;
Ok(InputSource::Stdin(content))
}
None => {
Err(InputError::NoTty)
}
}
}
pub fn process_input(source: InputSource) -> Result<String, Box<dyn std::error::Error>> {
let content = match source {
InputSource::File(c) | InputSource::Stdin(c) => c,
};
if content.trim_start().starts_with('#') || content.contains("\n#") {
Ok(content)
} else {
let mut markdown = String::from("# Input\n\n");
markdown.push_str(&content);
Ok(markdown)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_markdown_input() {
let markdown = "# Title\n\nContent here\n\n## Section\n";
let source = InputSource::Stdin(markdown.to_string());
let result = process_input(source).unwrap();
assert_eq!(result, markdown);
}
#[test]
fn test_process_plain_text() {
let text = "Just some plain text\nwith multiple lines";
let source = InputSource::Stdin(text.to_string());
let result = process_input(source).unwrap();
assert!(result.starts_with("# Input\n\n"));
assert!(result.contains("Just some plain text"));
}
}