agentmux_srv\backend/
trimquotes.rs

1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5//! Quote handling utilities for CLI argument processing.
6//! Port of Go's pkg/trimquotes/trimquotes.go.
7
8
9/// Remove surrounding double quotes from a string.
10/// Handles escape sequences within the quoted string.
11///
12/// Returns `(unquoted_string, success)`.
13/// If the string is not properly quoted, returns the original and `false`.
14///
15/// # Examples
16///
17/// ```
18/// use backend_test::backend::trimquotes::trim_quotes;
19///
20/// let (s, ok) = trim_quotes(r#""hello world""#);
21/// assert_eq!(s, "hello world");
22/// assert!(ok);
23///
24/// let (s, ok) = trim_quotes("unquoted");
25/// assert_eq!(s, "unquoted");
26/// assert!(!ok);
27/// ```
28pub fn trim_quotes(s: &str) -> (String, bool) {
29    // Go uses `len(s) > 2` — only unquote strings longer than 2 chars.
30    // This means `""` (empty quoted string) returns (s, false).
31    if s.len() <= 2 || !s.starts_with('"') {
32        return (s.to_string(), false);
33    }
34
35    match unescape_quoted(s) {
36        Some(unquoted) => (unquoted, true),
37        None => (s.to_string(), false),
38    }
39}
40
41/// Convenience wrapper that ignores the success flag.
42/// Always returns a string, falling back to the original on failure.
43pub fn try_trim_quotes(s: &str) -> String {
44    trim_quotes(s).0
45}
46
47/// Conditionally wrap a string in double quotes with proper escaping.
48pub fn replace_quotes(s: &str, should_replace: bool) -> String {
49    if should_replace {
50        escape_and_quote(s)
51    } else {
52        s.to_string()
53    }
54}
55
56/// Unescape a double-quoted string (like Go's strconv.Unquote).
57/// The input must start and end with `"`.
58fn unescape_quoted(s: &str) -> Option<String> {
59    if !s.starts_with('"') || !s.ends_with('"') || s.len() < 2 {
60        return None;
61    }
62
63    let inner = &s[1..s.len() - 1];
64    let mut result = String::with_capacity(inner.len());
65    let mut chars = inner.chars();
66
67    while let Some(c) = chars.next() {
68        if c == '\\' {
69            match chars.next() {
70                Some('\\') => result.push('\\'),
71                Some('"') => result.push('"'),
72                Some('n') => result.push('\n'),
73                Some('r') => result.push('\r'),
74                Some('t') => result.push('\t'),
75                Some('0') => result.push('\0'),
76                Some('a') => result.push('\x07'), // bell
77                Some('b') => result.push('\x08'), // backspace
78                Some('f') => result.push('\x0C'), // form feed
79                Some('v') => result.push('\x0B'), // vertical tab
80                Some(other) => {
81                    // Unknown escape: keep as-is
82                    result.push('\\');
83                    result.push(other);
84                }
85                None => return None, // trailing backslash
86            }
87        } else if c == '"' {
88            // Unescaped quote inside — invalid
89            return None;
90        } else {
91            result.push(c);
92        }
93    }
94
95    Some(result)
96}
97
98/// Escape a string and wrap in double quotes.
99fn escape_and_quote(s: &str) -> String {
100    let mut result = String::with_capacity(s.len() + 2);
101    result.push('"');
102    for c in s.chars() {
103        match c {
104            '"' => result.push_str("\\\""),
105            '\\' => result.push_str("\\\\"),
106            '\n' => result.push_str("\\n"),
107            '\r' => result.push_str("\\r"),
108            '\t' => result.push_str("\\t"),
109            '\0' => result.push_str("\\0"),
110            _ => result.push(c),
111        }
112    }
113    result.push('"');
114    result
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_trim_quotes_basic() {
123        let (s, ok) = trim_quotes(r#""hello""#);
124        assert_eq!(s, "hello");
125        assert!(ok);
126    }
127
128    #[test]
129    fn test_trim_quotes_with_spaces() {
130        let (s, ok) = trim_quotes(r#""hello world""#);
131        assert_eq!(s, "hello world");
132        assert!(ok);
133    }
134
135    #[test]
136    fn test_trim_quotes_with_escapes() {
137        let (s, ok) = trim_quotes(r#""hello\nworld""#);
138        assert_eq!(s, "hello\nworld");
139        assert!(ok);
140    }
141
142    #[test]
143    fn test_trim_quotes_escaped_quote() {
144        let (s, ok) = trim_quotes(r#""hello\"world""#);
145        assert_eq!(s, "hello\"world");
146        assert!(ok);
147    }
148
149    #[test]
150    fn test_trim_quotes_escaped_backslash() {
151        let (s, ok) = trim_quotes(r#""path\\to\\file""#);
152        assert_eq!(s, "path\\to\\file");
153        assert!(ok);
154    }
155
156    #[test]
157    fn test_trim_quotes_not_quoted() {
158        let (s, ok) = trim_quotes("unquoted");
159        assert_eq!(s, "unquoted");
160        assert!(!ok);
161    }
162
163    #[test]
164    fn test_trim_quotes_empty() {
165        let (s, ok) = trim_quotes("");
166        assert_eq!(s, "");
167        assert!(!ok);
168    }
169
170    #[test]
171    fn test_trim_quotes_single_char() {
172        let (s, ok) = trim_quotes("a");
173        assert_eq!(s, "a");
174        assert!(!ok);
175    }
176
177    #[test]
178    fn test_trim_quotes_empty_quoted() {
179        // Go behavior: `""` (2 chars) does NOT unquote — returns as-is with false.
180        let (s, ok) = trim_quotes(r#""""#);
181        assert_eq!(s, r#""""#);
182        assert!(!ok);
183    }
184
185    #[test]
186    fn test_try_trim_quotes() {
187        assert_eq!(try_trim_quotes(r#""test""#), "test");
188        assert_eq!(try_trim_quotes("unquoted"), "unquoted");
189    }
190
191    #[test]
192    fn test_replace_quotes_true() {
193        let result = replace_quotes("hello", true);
194        assert_eq!(result, r#""hello""#);
195    }
196
197    #[test]
198    fn test_replace_quotes_false() {
199        let result = replace_quotes("hello", false);
200        assert_eq!(result, "hello");
201    }
202
203    #[test]
204    fn test_replace_quotes_with_special() {
205        let result = replace_quotes("hello\nworld", true);
206        assert_eq!(result, r#""hello\nworld""#);
207    }
208
209    #[test]
210    fn test_replace_quotes_with_quotes() {
211        let result = replace_quotes(r#"say "hi""#, true);
212        assert_eq!(result, r#""say \"hi\"""#);
213    }
214
215    #[test]
216    fn test_escape_and_quote_roundtrip() {
217        let original = "hello \"world\"\nnew\\line";
218        let quoted = escape_and_quote(original);
219        let (unquoted, ok) = trim_quotes(&quoted);
220        assert!(ok);
221        assert_eq!(unquoted, original);
222    }
223
224    #[test]
225    fn test_all_escape_sequences() {
226        let (s, ok) = trim_quotes(r#""tab\there\nnewline\rreturn\0null""#);
227        assert!(ok);
228        assert!(s.contains('\t'));
229        assert!(s.contains('\n'));
230        assert!(s.contains('\r'));
231        assert!(s.contains('\0'));
232    }
233
234    #[test]
235    fn test_trailing_backslash_fails() {
236        let (s, ok) = trim_quotes(r#""trailing\"#);
237        assert!(!ok);
238        assert_eq!(s, r#""trailing\"#);
239    }
240}