#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
pub text: String,
pub from_file: bool,
}
pub fn resolve(raw: &str) -> Result<Resolved, String> {
if let Some(path) = raw.strip_prefix("file:") {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("reading payload file '{path}': {e}"))?;
Ok(Resolved {
text,
from_file: true,
})
} else if let Some(rest) = raw.strip_prefix("text:") {
Ok(Resolved {
text: rest.to_string(),
from_file: false,
})
} else {
Ok(Resolved {
text: raw.to_string(),
from_file: false,
})
}
}
pub fn to_lines(payload: &str) -> Vec<String> {
if payload.is_empty() {
return Vec::new();
}
let body = payload.strip_suffix('\n').unwrap_or(payload);
body.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l).to_string())
.collect()
}
pub fn to_find_lines(payload: &str) -> Vec<String> {
let mut lines = to_lines(payload);
while lines.last().is_some_and(String::is_empty) {
lines.pop();
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unprefixed_values_pass_through_verbatim() {
for raw in ["plain", "http://x/y", "std::fmt", "a file: in the middle"] {
let r = resolve(raw).unwrap();
assert_eq!(r.text, raw);
assert!(!r.from_file);
}
}
#[test]
fn text_prefix_strips_once_and_only_once() {
assert_eq!(resolve("text:text:x").unwrap().text, "text:x");
assert_eq!(resolve("text:").unwrap().text, "");
}
#[test]
fn file_prefix_reads_exact_bytes() {
let dir = std::env::temp_dir().join("ct-payload-test");
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("payload.block");
std::fs::write(&p, " indented(line),\nnext\n").unwrap();
let r = resolve(&format!("file:{}", p.display())).unwrap();
assert!(r.from_file);
assert_eq!(r.text, " indented(line),\nnext\n");
}
#[test]
fn missing_payload_file_is_an_error() {
assert!(resolve("file:/no/such/payload").is_err());
}
#[test]
fn to_lines_treats_one_trailing_newline_as_a_terminator() {
assert_eq!(to_lines("foo\n"), vec!["foo"]);
assert_eq!(to_lines("a\nb"), vec!["a", "b"]);
assert_eq!(to_lines("a\n\n"), vec!["a", ""]);
assert!(to_lines("").is_empty());
}
#[test]
fn to_lines_normalizes_crlf_to_lf() {
assert_eq!(to_lines("a\r\nb\r\n"), vec!["a", "b"]);
assert_eq!(to_lines("solo\r\n"), vec!["solo"]);
assert_eq!(to_lines("a\rb\n"), vec!["a\rb"]);
assert_eq!(to_lines("a\r\n\r\n"), vec!["a", ""]);
}
#[test]
fn to_find_lines_drops_trailing_blank_lines() {
assert_eq!(to_find_lines("a\nb\n\n"), vec!["a", "b"]);
assert_eq!(to_find_lines("a\nb\n\n\n"), vec!["a", "b"]);
assert_eq!(to_find_lines("a\r\nb\r\n\r\n"), vec!["a", "b"]);
assert_eq!(to_find_lines("a\nb\n"), vec!["a", "b"]);
assert_eq!(to_find_lines("a\n\nb\n"), vec!["a", "", "b"]);
assert_eq!(to_find_lines("a\n \n"), vec!["a", " "]);
assert!(to_find_lines("\n\n").is_empty());
assert!(to_find_lines("").is_empty());
}
}