Skip to main content

use_toml/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// A discovered TOML table header.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct TomlTable {
7    pub name: String,
8    pub line: usize,
9}
10
11/// A discovered TOML key-value pair.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct TomlKeyValue {
14    pub key: String,
15    pub value: String,
16    pub line: usize,
17}
18
19/// Returns `true` when the input contains TOML-looking table or key-value lines.
20pub 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
31/// Returns `true` when a line is a TOML table header.
32pub 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
42/// Returns `true` when a line is a TOML array-table header.
43pub 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
51/// Extracts TOML table headers from the input.
52pub 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
73/// Extracts TOML key-value pairs from the input.
74pub 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
90/// Splits a TOML key-value line on the first `=` outside quotes.
91pub 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
138/// Quotes a string as a TOML basic string.
139pub fn quote_toml_string(input: &str) -> String {
140    format!("\"{}\"", escape_toml_basic(input))
141}
142
143/// Unquotes a conservative TOML string literal.
144pub 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}