Skip to main content

hardware_enclave/internal/core/
quoting.rs

1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
2// Copyright 2026 Jay Gowdy
3// SPDX-License-Identifier: MIT
4
5//! Path and value quoting utilities for config file generation.
6//!
7//! Different config file formats have different quoting requirements:
8//! - AWS credential_process: backslash/quote escaping, double-quote wrapping
9//! - SSH config: forward-slash normalization on Windows, space-triggered quoting
10//! - Shell config: no special quoting (values are shell expressions)
11
12/// Quote a value for embedding in a config file.
13///
14/// If the value contains whitespace, double quotes, or backslashes, it is
15/// escaped and wrapped in double quotes. Otherwise returned as-is.
16///
17/// Suitable for AWS `credential_process`, INI-style configs, and similar formats.
18pub fn quote_config_value(value: &str) -> String {
19    if value.is_empty() {
20        return "\"\"".to_string();
21    }
22
23    let needs_quoting = value
24        .chars()
25        .any(|c| c.is_whitespace() || c == '"' || c == '\\');
26
27    if needs_quoting {
28        let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
29        format!("\"{escaped}\"")
30    } else {
31        value.to_string()
32    }
33}
34
35/// Quote a path for SSH config files.
36///
37/// On Windows, backslashes are converted to forward slashes (OpenSSH parser
38/// requirement). Paths containing spaces are wrapped in double quotes.
39pub fn quote_ssh_path(path: &std::path::Path) -> String {
40    let s = path.display().to_string();
41
42    #[cfg(windows)]
43    let s = s.replace('\\', "/");
44
45    if s.contains(' ') {
46        format!("\"{s}\"")
47    } else {
48        s
49    }
50}
51
52/// Quote a path specifically for the `credential_process` directive in AWS config.
53///
54/// This is identical to [`quote_config_value`] but takes a `Path` for convenience.
55pub fn quote_credential_process_arg(path: &std::path::Path) -> String {
56    quote_config_value(&path.display().to_string())
57}
58
59#[cfg(test)]
60#[allow(clippy::unwrap_used, clippy::panic)]
61mod tests {
62    use super::*;
63    use std::path::PathBuf;
64
65    #[test]
66    fn quote_config_value_no_quoting_needed() {
67        assert_eq!(quote_config_value("simple-path"), "simple-path");
68        assert_eq!(quote_config_value("/usr/bin/awsenc"), "/usr/bin/awsenc");
69    }
70
71    #[test]
72    fn quote_config_value_empty() {
73        assert_eq!(quote_config_value(""), "\"\"");
74    }
75
76    #[test]
77    fn quote_config_value_with_spaces() {
78        assert_eq!(
79            quote_config_value("/Program Files/app/bin"),
80            "\"/Program Files/app/bin\""
81        );
82    }
83
84    #[test]
85    fn quote_config_value_with_backslashes() {
86        assert_eq!(
87            quote_config_value("C:\\Users\\jay\\bin"),
88            "\"C:\\\\Users\\\\jay\\\\bin\""
89        );
90    }
91
92    #[test]
93    fn quote_config_value_with_quotes() {
94        assert_eq!(
95            quote_config_value("value with \"quotes\""),
96            "\"value with \\\"quotes\\\"\""
97        );
98    }
99
100    #[test]
101    fn quote_config_value_mixed() {
102        assert_eq!(
103            quote_config_value("C:\\Program Files\\app"),
104            "\"C:\\\\Program Files\\\\app\""
105        );
106    }
107
108    #[test]
109    fn quote_ssh_path_simple() {
110        let path = PathBuf::from("/home/user/.sshenc/agent.sock");
111        assert_eq!(quote_ssh_path(&path), "/home/user/.sshenc/agent.sock");
112    }
113
114    #[test]
115    fn quote_ssh_path_with_spaces() {
116        let path = PathBuf::from("/home/my user/.sshenc/agent.sock");
117        assert_eq!(
118            quote_ssh_path(&path),
119            "\"/home/my user/.sshenc/agent.sock\""
120        );
121    }
122
123    #[test]
124    fn quote_credential_process_simple() {
125        let path = PathBuf::from("/usr/local/bin/awsenc");
126        assert_eq!(quote_credential_process_arg(&path), "/usr/local/bin/awsenc");
127    }
128
129    #[test]
130    fn quote_credential_process_with_spaces() {
131        let path = PathBuf::from("/Program Files/awsenc/awsenc");
132        assert_eq!(
133            quote_credential_process_arg(&path),
134            "\"/Program Files/awsenc/awsenc\""
135        );
136    }
137
138    #[test]
139    fn quote_config_value_with_tab() {
140        // Tab is whitespace, must be quoted
141        let result = quote_config_value("key\tvalue");
142        assert!(result.starts_with('"'));
143        assert!(result.ends_with('"'));
144    }
145
146    #[test]
147    fn quote_config_value_only_backslash() {
148        assert_eq!(quote_config_value("\\"), "\"\\\\\"");
149    }
150
151    #[test]
152    fn quote_config_value_only_double_quote() {
153        assert_eq!(quote_config_value("\""), "\"\\\"\"");
154    }
155
156    #[test]
157    fn quote_config_value_backslash_and_space() {
158        // Both backslash and space present: both must be escaped
159        let result = quote_config_value("C:\\Program Files");
160        assert!(result.starts_with('"'));
161        assert!(result.contains("\\\\"));
162        assert!(result.contains(' '));
163    }
164
165    #[test]
166    fn quote_config_value_newline_is_quoted() {
167        // Newline is whitespace
168        let result = quote_config_value("line1\nline2");
169        assert!(result.starts_with('"'));
170    }
171
172    #[test]
173    fn quote_ssh_path_no_spaces_not_quoted() {
174        let path = PathBuf::from("/home/user/bin");
175        let result = quote_ssh_path(&path);
176        assert!(!result.starts_with('"'));
177        assert_eq!(result, "/home/user/bin");
178    }
179
180    #[test]
181    fn quote_credential_process_backslash_path() {
182        // Backslash triggers quoting regardless of spaces
183        let path = PathBuf::from("C:\\Windows\\System32\\app.exe");
184        let result = quote_credential_process_arg(&path);
185        assert!(result.starts_with('"'));
186        // Backslashes should be doubled
187        assert!(result.contains("\\\\"));
188    }
189}