Skip to main content

openjd_expr/functions/
repr.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5//! Repr function implementations (repr_py, repr_json, repr_sh, repr_cmd, repr_pwsh).
6
7use crate::error::ExpressionError;
8use crate::function_library::EvalContext;
9use crate::value::ExprValue;
10
11type R = Result<ExprValue, ExpressionError>;
12type Ctx<'a> = &'a mut dyn EvalContext;
13
14fn repr_string_len(a: &ExprValue) -> usize {
15    match a {
16        ExprValue::String(s) => s.len(),
17        ExprValue::Path { value, .. } => value.len(),
18        _ => 0,
19    }
20}
21
22pub fn repr_py_fn(ctx: Ctx, a: &[ExprValue]) -> R {
23    ctx.count_string_ops(repr_string_len(&a[0]))?;
24    Ok(ExprValue::String(repr_py(&a[0])))
25}
26
27pub fn repr_json_fn(ctx: Ctx, a: &[ExprValue]) -> R {
28    ctx.count_string_ops(repr_string_len(&a[0]))?;
29    Ok(ExprValue::String(repr_json(&a[0])))
30}
31
32pub fn repr_sh_fn(ctx: Ctx, a: &[ExprValue]) -> R {
33    if a[0].is_list() {
34        ctx.count_ops(a[0].list_len().unwrap_or(0))?;
35    }
36    ctx.count_string_ops(repr_string_len(&a[0]))?;
37    repr_sh(&a[0]).map(ExprValue::String)
38}
39
40pub fn repr_cmd_fn(ctx: Ctx, a: &[ExprValue]) -> R {
41    ctx.count_string_ops(repr_string_len(&a[0]))?;
42    Ok(ExprValue::String(repr_cmd(&a[0])))
43}
44
45pub fn repr_pwsh_fn(ctx: Ctx, a: &[ExprValue]) -> R {
46    ctx.count_string_ops(repr_string_len(&a[0]))?;
47    Ok(ExprValue::String(repr_pwsh(&a[0])))
48}
49
50fn repr_py(val: &ExprValue) -> String {
51    match val {
52        ExprValue::String(s) => format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'")),
53        ExprValue::Bool(b) => if *b { "True" } else { "False" }.to_string(),
54        ExprValue::Null => "None".to_string(),
55        ExprValue::Int(i) => i.to_string(),
56        ExprValue::Float(f) => {
57            if f.value().fract() == 0.0 {
58                format!("{:.1}", f)
59            } else {
60                f.to_string()
61            }
62        }
63        val if val.is_list() => {
64            let iter = val.list_iter().expect("is_list() was true");
65            format!(
66                "[{}]",
67                iter.map(|e| repr_py(&e)).collect::<Vec<_>>().join(", ")
68            )
69        }
70        ExprValue::Path { value, .. } => {
71            format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'"))
72        }
73        ExprValue::RangeExpr(r) => format!("'{r}'"),
74        _ => val.to_display_string(),
75    }
76}
77
78/// Escape a string for JSON output, matching Python's `json.dumps(ensure_ascii=True)`.
79/// All non-ASCII characters are encoded as `\uXXXX` (with surrogate pairs for chars > U+FFFF).
80fn json_escape(s: &str) -> String {
81    use std::fmt::Write;
82    let mut out = String::with_capacity(s.len());
83    for c in s.chars() {
84        match c {
85            '"' => out.push_str("\\\""),
86            '\\' => out.push_str("\\\\"),
87            '\x08' => out.push_str("\\b"),
88            '\x0c' => out.push_str("\\f"),
89            '\n' => out.push_str("\\n"),
90            '\r' => out.push_str("\\r"),
91            '\t' => out.push_str("\\t"),
92            c if (c as u32) < 0x20 => {
93                let _ = write!(out, "\\u{:04x}", c as u32);
94            }
95            c if c.is_ascii() => out.push(c),
96            c => {
97                let mut buf = [0u16; 2];
98                for unit in c.encode_utf16(&mut buf) {
99                    let _ = write!(out, "\\u{:04x}", unit);
100                }
101            }
102        }
103    }
104    out
105}
106
107fn repr_json(val: &ExprValue) -> String {
108    match val {
109        ExprValue::String(s) => format!("\"{}\"", json_escape(s)),
110        ExprValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
111        ExprValue::Null => "null".to_string(),
112        ExprValue::Int(i) => i.to_string(),
113        ExprValue::Float(f) => {
114            if f.value().fract() == 0.0 {
115                format!("{:.1}", f)
116            } else {
117                f.to_string()
118            }
119        }
120        val if val.is_list() => {
121            let iter = val.list_iter().expect("guard ensures list");
122            format!(
123                "[{}]",
124                iter.map(|e| repr_json(&e)).collect::<Vec<_>>().join(", ")
125            )
126        }
127        ExprValue::Path { value, .. } => format!("\"{}\"", json_escape(value)),
128        ExprValue::RangeExpr(r) => format!("\"{r}\""),
129        _ => val.to_display_string(),
130    }
131}
132
133fn repr_sh(val: &ExprValue) -> Result<String, ExpressionError> {
134    match val {
135        ExprValue::String(s) => shlex_quote(s),
136        ExprValue::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
137        ExprValue::Null => Ok("''".to_string()),
138        ExprValue::Int(i) => Ok(i.to_string()),
139        ExprValue::Float(f) => Ok(if f.value().fract() == 0.0 {
140            format!("{:.1}", f)
141        } else {
142            f.to_string()
143        }),
144        ExprValue::Path { value, .. } => shlex_quote(value),
145        val if val.is_list() => {
146            let strs: Vec<String> = val
147                .list_iter()
148                .expect("guard ensures list")
149                .map(|e| match &e {
150                    ExprValue::String(s) | ExprValue::Path { value: s, .. } => s.clone(),
151                    other => other.to_display_string(),
152                })
153                .collect();
154            shlex::try_join(strs.iter().map(|s| s.as_str()))
155                .map_err(|e| ExpressionError::new(format!("Cannot shell-quote list: {e}")))
156        }
157        _ => Ok(val.to_display_string()),
158    }
159}
160
161/// Shell-quote a single string, returning an error on null bytes.
162fn shlex_quote(s: &str) -> Result<String, ExpressionError> {
163    shlex::try_quote(s)
164        .map(|c| c.into_owned())
165        .map_err(|e| ExpressionError::new(format!("Cannot shell-quote string: {e}")))
166}
167
168fn repr_cmd(val: &ExprValue) -> String {
169    match val {
170        ExprValue::String(s) => cmd_quote(s),
171        ExprValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
172        ExprValue::Null => "\"\"".to_string(),
173        ExprValue::Int(i) => i.to_string(),
174        ExprValue::Float(f) => {
175            if f.value().fract() == 0.0 {
176                format!("{:.1}", f)
177            } else {
178                f.to_string()
179            }
180        }
181        ExprValue::Path { value, .. } => cmd_quote(value),
182        val if val.is_list() => val
183            .list_iter()
184            .expect("guard ensures list")
185            .map(|e| match &e {
186                ExprValue::String(s) | ExprValue::Path { value: s, .. } => cmd_quote(s),
187                other => other.to_display_string(),
188            })
189            .collect::<Vec<_>>()
190            .join(" "),
191        _ => val.to_display_string(),
192    }
193}
194
195fn repr_pwsh(val: &ExprValue) -> String {
196    match val {
197        ExprValue::String(s) => format!("'{}'", s.replace('\'', "''")),
198        ExprValue::Bool(b) => if *b { "$true" } else { "$false" }.to_string(),
199        ExprValue::Null => "$null".to_string(),
200        ExprValue::Int(i) => i.to_string(),
201        ExprValue::Float(f) => {
202            if f.value().fract() == 0.0 {
203                format!("{:.1}", f)
204            } else {
205                f.to_string()
206            }
207        }
208        ExprValue::Path { value, .. } => format!("'{}'", value.replace('\'', "''")),
209        val if val.is_list() => {
210            let items: Vec<String> = val
211                .list_iter()
212                .expect("guard ensures list")
213                .map(|e| match &e {
214                    ExprValue::String(s) | ExprValue::Path { value: s, .. } => {
215                        format!("'{}'", s.replace('\'', "''"))
216                    }
217                    ExprValue::Bool(b) => if *b { "$true" } else { "$false" }.to_string(),
218                    ExprValue::Null => "$null".to_string(),
219                    ExprValue::Int(i) => i.to_string(),
220                    other => other.to_display_string(),
221                })
222                .collect();
223            format!("@({})", items.join(", "))
224        }
225        ExprValue::RangeExpr(r) => format!("'{r}'"),
226        _ => val.to_display_string(),
227    }
228}
229
230fn cmd_quote(s: &str) -> String {
231    // Strip newlines: cmd.exe has no escape for a literal newline inside a quoted argument,
232    // and an embedded newline would cause cmd.exe to treat the remainder as a new command
233    // (a command injection vector). See EXPR specification §2.2.6.
234    let stripped: String = s.chars().filter(|c| *c != '\n' && *c != '\r').collect();
235    const NEEDS_QUOTING: &str = " \t&|<>^\"()%!";
236    if !stripped.is_empty() && !stripped.chars().any(|c| NEEDS_QUOTING.contains(c)) {
237        return stripped;
238    }
239    let mut escaped = String::with_capacity(stripped.len());
240    for c in stripped.chars() {
241        match c {
242            '^' | '"' => {
243                escaped.push('^');
244                escaped.push(c);
245            }
246            '%' => escaped.push_str("%%"),
247            // Escape ! as ^^! for EnableDelayedExpansion contexts: cmd.exe processes ^ escapes
248            // before delayed expansion, so a single ^ is consumed and leaves a bare !.
249            '!' => escaped.push_str("^^!"),
250            c => escaped.push(c),
251        }
252    }
253    format!("\"{escaped}\"")
254}