1const fn cq_byte(b: u8) -> i8 {
14 match b {
15 0x00..=0x06 => 1,
16 0x07 => b'a' as i8,
17 0x08 => b'b' as i8,
18 0x09 => b't' as i8,
19 0x0a => b'n' as i8,
20 0x0b => b'v' as i8,
21 0x0c => b'f' as i8,
22 0x0d => b'r' as i8,
23 0x0e..=0x1f => 1,
24 0x20 | 0x21 => -1,
25 0x22 => b'"' as i8,
26 0x23..=0x5b => -1,
27 0x5c => b'\\' as i8,
28 0x5d..=0x7e => -1,
29 0x7f => 1,
30 0x80..=0xff => 0,
31 }
32}
33
34const fn cq_lookup_table() -> [i8; 256] {
35 let mut t = [0i8; 256];
36 let mut i = 0usize;
37 while i < 256 {
38 t[i] = cq_byte(i as u8);
39 i += 1;
40 }
41 t
42}
43
44static CQ_LOOKUP: [i8; 256] = cq_lookup_table();
45
46#[inline]
47fn cq_must_quote(byte: u8, quote_fully: bool) -> bool {
48 i32::from(CQ_LOOKUP[byte as usize]) + i32::from(quote_fully) > 0
49}
50
51fn quote_c_style_inner(path: &str, quote_fully: bool, force_quotes: bool) -> String {
52 let bytes = path.as_bytes();
53 let mut any = force_quotes;
54 if !any {
55 for &b in bytes {
56 if cq_must_quote(b, quote_fully) {
57 any = true;
58 break;
59 }
60 }
61 }
62 if !any {
63 return path.to_owned();
64 }
65
66 let mut out = String::with_capacity(path.len() + 2);
67 out.push('"');
68 let mut p = 0usize;
69 while p < bytes.len() {
70 let mut len = 0usize;
71 while p + len < bytes.len() && !cq_must_quote(bytes[p + len], quote_fully) {
72 len += 1;
73 }
74 out.push_str(path.get(p..p + len).unwrap_or(""));
75 p += len;
76 if p >= bytes.len() {
77 break;
78 }
79 let ch = bytes[p];
80 p += 1;
81 out.push('\\');
82 let cq = CQ_LOOKUP[ch as usize];
83 if cq >= b' ' as i8 {
84 out.push(cq as u8 as char);
85 } else {
86 out.push(char::from(((ch >> 6) & 3) + b'0'));
87 out.push(char::from(((ch >> 3) & 7) + b'0'));
88 out.push(char::from((ch & 7) + b'0'));
89 }
90 }
91 out.push('"');
92 out
93}
94
95#[must_use]
101pub fn quote_c_style(path: &str, quote_fully: bool) -> String {
102 quote_c_style_inner(path, quote_fully, false)
103}
104
105#[must_use]
108pub fn quote_path_for_tree_listing(path: &str, quote_fully: bool) -> String {
109 let force = path.as_bytes().contains(&b',');
110 quote_c_style_inner(path, quote_fully, force)
111}
112
113#[must_use]
116pub fn format_diff_path_with_prefix(prefix: &str, path: &str, quote_fully: bool) -> String {
117 let quoted = quote_c_style(path, quote_fully);
118 if quoted == path {
119 format!("{prefix}{path}")
120 } else {
121 let inner = quoted
122 .strip_prefix('"')
123 .and_then(|s| s.strip_suffix('"'))
124 .unwrap_or(path);
125 format!("\"{prefix}{inner}\"")
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn ascii_safe_unchanged() {
135 assert_eq!(quote_c_style("Name", true), "Name");
136 assert_eq!(quote_c_style("With SP in it", true), "With SP in it");
137 }
138
139 #[test]
140 fn t3902_expect_quoted() {
141 assert_eq!(quote_c_style("Name and a\nLF", true), "\"Name and a\\nLF\"");
142 assert_eq!(
143 quote_c_style("Name and an\tHT", true),
144 "\"Name and an\\tHT\""
145 );
146 assert_eq!(quote_c_style("Name\"", true), "\"Name\\\"\"");
147 }
148
149 #[test]
150 fn t3902_expect_raw_mode() {
151 let s = "濱野\t純";
152 assert_eq!(quote_c_style(s, false), "\"濱野\\t純\"");
153 let s2 = "濱野 純";
154 assert_eq!(quote_c_style(s2, false), "濱野 純");
155 }
156
157 #[test]
158 fn comma_forces_ls_tree_style_quotes() {
159 assert_eq!(quote_path_for_tree_listing("a,b", true), "\"a,b\"");
160 assert_eq!(quote_c_style("a,b", true), "a,b");
161 }
162
163 #[test]
164 fn diff_git_prefix_quoting() {
165 let p = "tabs\t,\" (dq) and spaces";
166 assert_eq!(
167 format_diff_path_with_prefix("a/", p, true),
168 "\"a/tabs\\t,\\\" (dq) and spaces\""
169 );
170 assert_eq!(format_diff_path_with_prefix("b/", "plain", true), "b/plain");
171 }
172}