1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct TomlTable {
7 pub name: String,
8 pub line: usize,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct TomlKeyValue {
14 pub key: String,
15 pub value: String,
16 pub line: usize,
17}
18
19pub fn looks_like_toml(input: &str) -> bool {
21 input.lines().any(|line| {
22 let trimmed = line.trim();
23 !trimmed.is_empty()
24 && !trimmed.starts_with('#')
25 && (is_toml_table(trimmed)
26 || is_toml_array_table(trimmed)
27 || split_toml_key_value(trimmed).is_some())
28 })
29}
30
31pub fn is_toml_table(line: &str) -> bool {
33 let trimmed = line.trim();
34 trimmed.len() >= 3
35 && trimmed.starts_with('[')
36 && trimmed.ends_with(']')
37 && !trimmed.starts_with("[[")
38 && !trimmed.ends_with("]]")
39 && !trimmed[1..trimmed.len() - 1].trim().is_empty()
40}
41
42pub fn is_toml_array_table(line: &str) -> bool {
44 let trimmed = line.trim();
45 trimmed.len() >= 5
46 && trimmed.starts_with("[[")
47 && trimmed.ends_with("]]")
48 && !trimmed[2..trimmed.len() - 2].trim().is_empty()
49}
50
51pub fn extract_toml_tables(input: &str) -> Vec<TomlTable> {
53 let mut tables = Vec::new();
54
55 for (line_index, line) in input.lines().enumerate() {
56 let trimmed = line.trim();
57 if is_toml_table(trimmed) {
58 tables.push(TomlTable {
59 name: trimmed[1..trimmed.len() - 1].trim().to_string(),
60 line: line_index + 1,
61 });
62 } else if is_toml_array_table(trimmed) {
63 tables.push(TomlTable {
64 name: trimmed[2..trimmed.len() - 2].trim().to_string(),
65 line: line_index + 1,
66 });
67 }
68 }
69
70 tables
71}
72
73pub fn extract_toml_key_values(input: &str) -> Vec<TomlKeyValue> {
75 let mut pairs = Vec::new();
76
77 for (line_index, line) in input.lines().enumerate() {
78 if let Some((key, value)) = split_toml_key_value(line) {
79 pairs.push(TomlKeyValue {
80 key,
81 value,
82 line: line_index + 1,
83 });
84 }
85 }
86
87 pairs
88}
89
90pub fn split_toml_key_value(line: &str) -> Option<(String, String)> {
92 let content = strip_toml_comment(line).trim();
93 if content.is_empty() || is_toml_table(content) || is_toml_array_table(content) {
94 return None;
95 }
96
97 let mut in_single = false;
98 let mut in_double = false;
99 let mut escaped = false;
100
101 for (index, ch) in content.char_indices() {
102 if in_single {
103 if ch == '\'' {
104 in_single = false;
105 }
106 continue;
107 }
108
109 if in_double {
110 if escaped {
111 escaped = false;
112 } else if ch == '\\' {
113 escaped = true;
114 } else if ch == '"' {
115 in_double = false;
116 }
117 continue;
118 }
119
120 match ch {
121 '\'' => in_single = true,
122 '"' => in_double = true,
123 '=' => {
124 let key = content[..index].trim();
125 let value = content[index + 1..].trim();
126 if key.is_empty() || value.is_empty() {
127 return None;
128 }
129 return Some((key.to_string(), value.to_string()));
130 },
131 _ => {},
132 }
133 }
134
135 None
136}
137
138pub fn quote_toml_string(input: &str) -> String {
140 format!("\"{}\"", escape_toml_basic(input))
141}
142
143pub fn unquote_toml_string(input: &str) -> Option<String> {
145 let trimmed = input.trim();
146
147 if trimmed.len() < 2 {
148 return None;
149 }
150
151 if trimmed.starts_with('"') && trimmed.ends_with('"') {
152 return unquote_toml_basic(trimmed);
153 }
154
155 if trimmed.starts_with('\'') && trimmed.ends_with('\'') {
156 let inner = &trimmed[1..trimmed.len() - 1];
157 if inner.contains('\'') {
158 return None;
159 }
160 return Some(inner.to_string());
161 }
162
163 None
164}
165
166fn strip_toml_comment(line: &str) -> &str {
167 let mut in_single = false;
168 let mut in_double = false;
169 let mut escaped = false;
170
171 for (index, ch) in line.char_indices() {
172 if in_single {
173 if ch == '\'' {
174 in_single = false;
175 }
176 continue;
177 }
178
179 if in_double {
180 if escaped {
181 escaped = false;
182 } else if ch == '\\' {
183 escaped = true;
184 } else if ch == '"' {
185 in_double = false;
186 }
187 continue;
188 }
189
190 match ch {
191 '\'' => in_single = true,
192 '"' => in_double = true,
193 '#' => return &line[..index],
194 _ => {},
195 }
196 }
197
198 line
199}
200
201fn escape_toml_basic(input: &str) -> String {
202 let mut escaped = String::with_capacity(input.len());
203
204 for ch in input.chars() {
205 match ch {
206 '"' => escaped.push_str("\\\""),
207 '\\' => escaped.push_str("\\\\"),
208 '\n' => escaped.push_str("\\n"),
209 '\r' => escaped.push_str("\\r"),
210 '\t' => escaped.push_str("\\t"),
211 ch if ch.is_control() => escaped.push_str(&format!("\\u{:04X}", ch as u32)),
212 _ => escaped.push(ch),
213 }
214 }
215
216 escaped
217}
218
219fn unquote_toml_basic(input: &str) -> Option<String> {
220 let inner = &input[1..input.len() - 1];
221 let mut chars = inner.chars();
222 let mut output = String::new();
223
224 while let Some(ch) = chars.next() {
225 if ch == '"' || ch.is_control() {
226 return None;
227 }
228
229 if ch != '\\' {
230 output.push(ch);
231 continue;
232 }
233
234 match chars.next()? {
235 '"' => output.push('"'),
236 '\\' => output.push('\\'),
237 'n' => output.push('\n'),
238 'r' => output.push('\r'),
239 't' => output.push('\t'),
240 'u' => {
241 let mut hex = String::with_capacity(4);
242 for _ in 0..4 {
243 hex.push(chars.next()?);
244 }
245 let value = u32::from_str_radix(&hex, 16).ok()?;
246 output.push(char::from_u32(value)?);
247 },
248 _ => return None,
249 }
250 }
251
252 Some(output)
253}