Skip to main content

onenote_cli/
write.rs

1use crate::{cli::ContentArgs, error::Error};
2use serde_json::{Value, json};
3use std::io::{IsTerminal, Read};
4
5pub const MAX_TEXT: usize = 1024 * 1024;
6
7pub fn validate(text: &str, title: bool) -> Result<(), Error> {
8    let max = if title { 512 } else { MAX_TEXT };
9    if text.trim().is_empty() || text.len() > max {
10        return Err(Error::invalid(format!(
11            "{} must contain 1–{max} bytes of text",
12            if title { "Title" } else { "Content" }
13        )));
14    }
15    if text.chars().any(|c| {
16        matches!(c, '\u{fffe}' | '\u{ffff}')
17            || (c.is_control() && (title || !matches!(c, '\n' | '\r' | '\t')))
18    }) {
19        return Err(Error::invalid(
20            "Text contains unsupported control or XML characters",
21        ));
22    }
23    Ok(())
24}
25
26pub fn content(args: &ContentArgs) -> Result<String, Error> {
27    let text = if let Some(text) = &args.text {
28        text.clone()
29    } else {
30        let path = args
31            .file
32            .as_ref()
33            .ok_or_else(|| Error::invalid("Use --text or --file"))?;
34        let reader: Box<dyn Read> = if path.as_os_str() == "-" {
35            if std::io::stdin().is_terminal() {
36                return Err(Error::invalid("Pipe UTF-8 text into --file -"));
37            }
38            Box::new(std::io::stdin())
39        } else {
40            Box::new(std::fs::File::open(path)?)
41        };
42        let mut bytes = Vec::new();
43        reader.take((MAX_TEXT + 1) as u64).read_to_end(&mut bytes)?;
44        if bytes.len() > MAX_TEXT {
45            return Err(Error::invalid("Content exceeds 1 MiB"));
46        }
47        String::from_utf8(bytes).map_err(|_| Error::invalid("Content must be UTF-8"))?
48    };
49    let text = text.strip_prefix('\u{feff}').unwrap_or(&text);
50    validate(text, false)?;
51    Ok(text.replace("\r\n", "\n").replace('\r', "\n"))
52}
53
54pub fn prepare(
55    operation: &str,
56    target: &str,
57    title: Option<&str>,
58    args: &ContentArgs,
59) -> Result<Value, Error> {
60    if let Some(title) = title {
61        validate(title, true)?;
62    }
63    let text = content(args)?;
64    Ok(json!({"operation":operation,"target":target,"title":title,"text":text}))
65}