use super::{CAP, OpEntry, Origin};
use serde_json::{Map, Value};
const ORIGIN: &str = "origin";
pub fn build_line(entry: &OpEntry) -> Vec<u8> {
let full = serialize(entry, &entry.stdout, &entry.stderr, false);
if full.len() <= CAP {
return full;
}
if serialize(entry, "", &entry.stderr, true).len() <= CAP {
let kept = largest_fit(entry.stdout.len(), |n| {
serialize(entry, head(&entry.stdout, n), &entry.stderr, true).len()
});
return serialize(entry, head(&entry.stdout, kept), &entry.stderr, true);
}
let bare = serialize(entry, "", "", true);
if bare.len() > CAP {
return bare;
}
let kept = largest_fit(entry.stderr.len(), |n| {
serialize(entry, "", head(&entry.stderr, n), true).len()
});
serialize(entry, "", head(&entry.stderr, kept), true)
}
fn serialize(entry: &OpEntry, stdout: &str, stderr: &str, truncated: bool) -> Vec<u8> {
let mut map = Map::new();
map.insert("ts".into(), Value::from(entry.ts.as_str()));
map.insert("argv".into(), Value::from(entry.argv.clone()));
map.insert("cwd".into(), Value::from(entry.cwd.as_str()));
map.insert("exit".into(), Value::from(entry.exit));
map.insert(ORIGIN.into(), Value::from(entry.origin.as_str()));
map.insert("stdout".into(), Value::from(stdout));
map.insert("stderr".into(), Value::from(stderr));
if truncated {
map.insert("truncated".into(), Value::Bool(true));
}
let mut bytes = serde_json::to_vec(&Value::Object(map)).unwrap_or_default();
bytes.push(b'\n');
bytes
}
fn largest_fit(max: usize, len: impl Fn(usize) -> usize) -> usize {
let (mut lo, mut hi) = (0usize, max);
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if len(mid) <= CAP {
lo = mid;
} else {
hi = mid - 1;
}
}
lo
}
pub fn clip_goal(entry: &OpEntry) -> OpEntry {
let Some((goal, head_argv)) = entry.argv.split_last() else {
return entry.clone();
};
if build_line(entry).len() <= CAP {
return entry.clone();
}
let rebuilt = |g: String| {
let mut argv = head_argv.to_vec();
argv.push(g);
OpEntry {
argv,
..entry.clone()
}
};
let kept = largest_fit(goal.len(), |n| {
build_line(&rebuilt(clip_arg(goal, n))).len()
});
rebuilt(clip_arg(goal, kept))
}
pub(super) fn clip_arg(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_owned();
}
let kept = head(s, max);
format!("{kept}… [+{} bytes elided]", s.len() - kept.len())
}
fn head(s: &str, max_bytes: usize) -> &str {
let mut i = max_bytes.min(s.len());
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
s.get(..i).unwrap_or(s)
}
pub(super) fn parse_line(line: &str) -> Option<OpEntry> {
let value: Value = serde_json::from_str(line).ok()?;
let obj = value.as_object()?;
Some(OpEntry {
ts: str_field(obj, "ts"),
argv: obj
.get("argv")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.map(String::from)
.collect()
})
.unwrap_or_default(),
cwd: str_field(obj, "cwd"),
exit: obj.get("exit").and_then(Value::as_i64).unwrap_or(0) as i32,
stdout: str_field(obj, "stdout"),
stderr: str_field(obj, "stderr"),
origin: Origin::parse(&str_field(obj, ORIGIN)),
})
}
fn str_field(obj: &Map<String, Value>, key: &str) -> String {
obj.get(key)
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
}
#[cfg(test)]
mod tests;