Skip to main content

jk_core/command_history/
result.rs

1use std::path::PathBuf;
2
3use super::redaction::redact_text;
4
5/// Process result summary retained by history.
6#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct CommandResultSummary {
8    /// Exit status summary when the process started and exited.
9    pub exit_status: Option<ExitStatusSummary>,
10    /// Spawn or runner error when no exit status was available.
11    pub spawn_error: Option<String>,
12    /// Bounded stdout summary.
13    pub stdout: StreamSummary,
14    /// Bounded stderr summary.
15    pub stderr: StreamSummary,
16}
17
18impl Default for CommandResultSummary {
19    fn default() -> Self {
20        Self {
21            exit_status: None,
22            spawn_error: None,
23            stdout: StreamSummary::empty(),
24            stderr: StreamSummary::empty(),
25        }
26    }
27}
28
29impl CommandResultSummary {
30    pub(super) fn apply_retention(&mut self, retention: &OutputRetention) {
31        self.stdout.apply_limit(retention.stdout_limit());
32        self.stderr.apply_limit(retention.stderr_limit());
33    }
34}
35
36/// Exit status data that does not require retaining platform-specific process types.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct ExitStatusSummary {
39    /// Numeric process exit code, when available.
40    pub code: Option<i32>,
41    /// Platform signal number, when available.
42    pub signal: Option<i32>,
43    /// Whether the command succeeded.
44    pub success: bool,
45}
46
47impl ExitStatusSummary {
48    /// Creates a normal exit-code status summary.
49    #[must_use]
50    pub const fn code(code: i32) -> Self {
51        Self {
52            code: Some(code),
53            signal: None,
54            success: code == 0,
55        }
56    }
57
58    /// Creates a signal status summary.
59    #[must_use]
60    pub const fn signal(signal: i32) -> Self {
61        Self {
62            code: None,
63            signal: Some(signal),
64            success: false,
65        }
66    }
67}
68
69/// Bounded output retention policy for a command record.
70#[derive(Clone, Debug, Eq, PartialEq)]
71#[non_exhaustive]
72pub enum OutputRetention {
73    /// Retain only bounded stdout and stderr summaries.
74    SummaryOnly {
75        /// Maximum stdout snippet bytes.
76        stdout_limit: usize,
77        /// Maximum stderr snippet bytes.
78        stderr_limit: usize,
79    },
80    /// Retain summaries and point to a local full-output artifact.
81    Artifact {
82        /// Maximum stdout snippet bytes.
83        stdout_limit: usize,
84        /// Maximum stderr snippet bytes.
85        stderr_limit: usize,
86        /// Local artifact path that owns full output.
87        full_output_path: PathBuf,
88    },
89}
90
91impl OutputRetention {
92    /// Creates a summary-only retention policy.
93    #[must_use]
94    pub const fn summary_only(stdout_limit: usize, stderr_limit: usize) -> Self {
95        Self::SummaryOnly {
96            stdout_limit,
97            stderr_limit,
98        }
99    }
100
101    const fn stdout_limit(&self) -> usize {
102        match self {
103            Self::SummaryOnly { stdout_limit, .. } | Self::Artifact { stdout_limit, .. } => {
104                *stdout_limit
105            }
106        }
107    }
108
109    const fn stderr_limit(&self) -> usize {
110        match self {
111            Self::SummaryOnly { stderr_limit, .. } | Self::Artifact { stderr_limit, .. } => {
112                *stderr_limit
113            }
114        }
115    }
116}
117
118/// Bounded and redacted stream summary.
119#[derive(Clone, Debug, Default, Eq, PartialEq)]
120pub struct StreamSummary {
121    /// Original stream byte length before redaction or truncation.
122    pub byte_len: usize,
123    /// Original stream line count before redaction or truncation.
124    pub line_count: usize,
125    /// Redacted snippet retained in memory.
126    pub snippet: String,
127    /// Whether the redacted snippet was truncated.
128    pub truncated: bool,
129    /// Whether any obvious secret-looking text was redacted.
130    pub redacted: bool,
131}
132
133impl StreamSummary {
134    /// Creates an empty stream summary.
135    #[must_use]
136    pub const fn empty() -> Self {
137        Self {
138            byte_len: 0,
139            line_count: 0,
140            snippet: String::new(),
141            truncated: false,
142            redacted: false,
143        }
144    }
145
146    /// Creates a bounded stream summary from bytes.
147    #[must_use]
148    pub fn from_bytes(bytes: &[u8], limit: usize) -> Self {
149        Self::from_text(&String::from_utf8_lossy(bytes), limit).with_byte_len(bytes.len())
150    }
151
152    /// Creates a bounded stream summary from text.
153    #[must_use]
154    pub fn from_text(text: &str, limit: usize) -> Self {
155        let byte_len = text.len();
156        let line_count = text.lines().count();
157        let (redacted_text, redacted) = redact_text(text);
158        let (snippet, truncated) = truncate_to_boundary(&redacted_text, limit);
159
160        Self {
161            byte_len,
162            line_count,
163            snippet,
164            truncated,
165            redacted,
166        }
167    }
168
169    const fn with_byte_len(mut self, byte_len: usize) -> Self {
170        self.byte_len = byte_len;
171        self
172    }
173
174    fn apply_limit(&mut self, limit: usize) {
175        let (snippet, truncated) = truncate_to_boundary(&self.snippet, limit);
176        self.truncated |= truncated;
177        self.snippet = snippet;
178    }
179}
180
181fn truncate_to_boundary(text: &str, limit: usize) -> (String, bool) {
182    if text.len() <= limit {
183        return (text.to_owned(), false);
184    }
185
186    if limit == 0 {
187        return (String::new(), true);
188    }
189
190    let mut end = limit;
191    while !text.is_char_boundary(end) {
192        end -= 1;
193    }
194    (text[..end].to_owned(), true)
195}