Skip to main content

nextjson/formats/
ini.rs

1//! INI codec (Windows-style configuration text).
2//!
3//! Document-shaped text format: a headerless "global" section followed by
4//! named `[section]` blocks of `key = value` lines.
5//!
6//! - Comments: `;` and `#` run to end of line.
7//! - Values are stringified scalars (numbers / booleans keep their textual
8//!   form); strings may be quoted with `'` (literal) or `"` (with `\\`,
9//!   `\"`, `\n`, `\t`, `\r` escapes).
10//! - The JSON model maps to INI as: a top-level object's scalar entries live
11//!   in the global section; its object entries become `[section]` blocks.
12//! - Arrays, `null` and nested sections are not representable and are
13//!   rejected honestly.
14//! - Repeated keys use the last value (common INI semantics).
15//!
16//! On encode, string values that look numeric or boolean are quoted so the
17//! round-trip is unambiguous; on decode, unquoted values are type-guessed
18//! back (`true`/`false`, integers, floats) while quoted values stay strings.
19//!
20//! Decode parses the whole document into a [`Value`] first (document-shaped),
21//! then serves the unified event stream from it — the same pattern as TOML.
22
23use alloc::string::{String, ToString};
24use alloc::vec::Vec;
25
26use crate::de::NsonDeserialize;
27use crate::error::{Error, Result};
28use crate::formats::tree;
29use crate::formats::Format;
30use crate::map::Map;
31use crate::number::Number;
32use crate::ser::NsonSerialize;
33use crate::value::Value;
34use crate::write::Write;
35
36/// INI format marker.
37#[derive(Clone, Copy, Debug)]
38pub struct Ini;
39
40/// Document-decoded INI decoder: parses the whole document into a [`Value`]
41/// and serves the unified event stream from it.
42pub type IniDecoder<'de> = tree::TreeDecoder<'de>;
43
44impl Format for Ini {
45    const NAME: &'static str = "ini";
46    const MIME: &'static str = "text/plain";
47    const EXTENSIONS: &'static [&'static str] = &["ini", "cfg", "conf"];
48    const BINARY: bool = false;
49
50    fn encode<T: NsonSerialize + ?Sized>(self, value: &T) -> Result<Vec<u8>> {
51        let mut encoder = IniEncoder::new(Vec::new());
52        let mut checked = crate::ser::CheckedEncoder::new(&mut encoder);
53        T::nextencode(value, &mut checked)?;
54        checked.finish()?;
55        encoder.finish()
56    }
57
58    fn decode<'de, T: NsonDeserialize<'de>>(self, input: &'de [u8]) -> Result<T> {
59        let value = parse_ini(input)?;
60        let mut decoder = tree::TreeDecoder::new(tree::value_to_tokens(&value)?);
61        let out = T::nextdecode(&mut decoder)?;
62        decoder.end()?;
63        Ok(out)
64    }
65}
66
67// ---------------------------------------------------------------------------
68// Encoder (collect into Value, emit at the end)
69// ---------------------------------------------------------------------------
70
71/// INI encoder that collects one event stream and emits it on [`finish`](Self::finish).
72pub struct IniEncoder<W: Write> {
73    writer: W,
74    collector: tree::CollectEncoder,
75}
76
77impl<W: Write> IniEncoder<W> {
78    /// Create an INI encoder over `writer`.
79    pub fn new(writer: W) -> Self {
80        Self {
81            writer,
82            collector: tree::CollectEncoder::new(),
83        }
84    }
85
86    /// Emit the collected document, flush, and return the writer.
87    pub fn finish(mut self) -> Result<W> {
88        let root = self.collector.take_root()?;
89        let mut out = Vec::with_capacity(256);
90        emit_ini(&root, &mut out)?;
91        self.writer.write_all(&out)?;
92        self.writer.flush()?;
93        Ok(self.writer)
94    }
95}
96
97impl<W: Write> crate::ser::FormatEncoder for IniEncoder<W> {
98    type Error = crate::error::Error;
99
100    fn begin_array(&mut self) -> Result<(), Self::Error> {
101        self.collector.begin_array()
102    }
103    fn separator(&mut self) -> Result<(), Self::Error> {
104        self.collector.separator()
105    }
106    fn end_array(&mut self) -> Result<(), Self::Error> {
107        self.collector.end_array()
108    }
109    fn begin_object(&mut self) -> Result<(), Self::Error> {
110        self.collector.begin_object()
111    }
112    fn key(&mut self, key: &str) -> Result<(), Self::Error> {
113        self.collector.key(key)
114    }
115    fn end_object(&mut self) -> Result<(), Self::Error> {
116        self.collector.end_object()
117    }
118    fn write_null(&mut self) -> Result<(), Self::Error> {
119        self.collector.write_null()
120    }
121    fn write_bool(&mut self, value: bool) -> Result<(), Self::Error> {
122        self.collector.write_bool(value)
123    }
124    fn write_str(&mut self, value: &str) -> Result<(), Self::Error> {
125        self.collector.write_str(value)
126    }
127    fn write_char(&mut self, value: char) -> Result<(), Self::Error> {
128        self.collector.write_char(value)
129    }
130    fn write_number(&mut self, value: &Number) -> Result<(), Self::Error> {
131        self.collector.write_number(value)
132    }
133    fn write_i64(&mut self, value: i64) -> Result<(), Self::Error> {
134        self.collector.write_i64(value)
135    }
136    fn write_u64(&mut self, value: u64) -> Result<(), Self::Error> {
137        self.collector.write_u64(value)
138    }
139    fn write_i128(&mut self, value: i128) -> Result<(), Self::Error> {
140        self.collector.write_i128(value)
141    }
142    fn write_u128(&mut self, value: u128) -> Result<(), Self::Error> {
143        self.collector.write_u128(value)
144    }
145    fn write_f64(&mut self, value: f64) -> Result<(), Self::Error> {
146        self.collector.write_f64(value)
147    }
148    fn write_f32(&mut self, value: f32) -> Result<(), Self::Error> {
149        self.collector.write_f32(value)
150    }
151    fn write_none(&mut self) -> Result<(), Self::Error> {
152        self.collector.write_none()
153    }
154    fn is_human_readable(&self) -> bool {
155        true
156    }
157}
158
159/// Stringify a scalar [`Value`] for an INI value slot.
160fn scalar_text(v: &Value) -> Result<String> {
161    match v {
162        Value::String(s) => Ok(s.clone()),
163        Value::Number(n) => tree::number_string(n),
164        Value::Bool(b) => Ok(b.to_string()),
165        other => Err(Error::custom(alloc::format!(
166            "ini: {other:?} is not representable as a scalar value"
167        ))),
168    }
169}
170
171/// Whether an unquoted string would be type-guessed into a non-string
172/// (number/boolean) on decode. A `Value::String` that looks numeric must be
173/// quoted on encode so the round-trip is unambiguous; genuine `Number` /
174/// `Bool` values are written bare so they can be guessed back.
175fn looks_non_string(value: &str) -> bool {
176    if matches!(value, "true" | "false") {
177        return true;
178    }
179    if parse_ini_int(value).is_ok() {
180        return true;
181    }
182    value.contains(['.', 'e', 'E'])
183        && value
184            .bytes()
185            .all(|b| b.is_ascii_digit() || matches!(b, b'.' | b'e' | b'E' | b'+' | b'-'))
186        && tree::parse_float(value).is_some()
187}
188
189/// Escape an INI value: quote only when necessary. `from_string` says the
190/// text originated from a `Value::String`; strings that look numeric/boolean
191/// must be quoted so the decoder guesses the string type back.
192fn emit_value(out: &mut Vec<u8>, value: &str, from_string: bool) {
193    let needs_quotes = value.contains(['=', ';', '#', '\n', '\r'])
194        || value.starts_with(char::is_whitespace)
195        || value.ends_with(char::is_whitespace)
196        || (from_string && looks_non_string(value));
197    if needs_quotes {
198        out.push(b'"');
199        for &b in value.as_bytes() {
200            match b {
201                b'\\' => out.extend_from_slice(b"\\\\"),
202                b'"' => out.extend_from_slice(b"\\\""),
203                b'\n' => out.extend_from_slice(b"\\n"),
204                b'\t' => out.extend_from_slice(b"\\t"),
205                b'\r' => out.extend_from_slice(b"\\r"),
206                other => out.push(other),
207            }
208        }
209        out.push(b'"');
210    } else {
211        out.extend_from_slice(value.as_bytes());
212    }
213}
214
215/// Emit the collected Value tree as INI text.
216fn emit_ini(root: &Value, out: &mut Vec<u8>) -> Result<()> {
217    let map = root
218        .as_object()
219        .ok_or_else(|| Error::custom("ini: root must be an object (a table)"))?;
220    // First pass: global-section scalars.
221    for (key, value) in map.iter() {
222        if matches!(value, Value::Object(_)) {
223            continue;
224        }
225        out.extend_from_slice(key.as_bytes());
226        out.extend_from_slice(b" = ");
227        emit_value(out, &scalar_text(value)?, matches!(value, Value::String(_)));
228        out.push(b'\n');
229    }
230    // Second pass: `[section]` blocks.
231    for (key, value) in map.iter() {
232        let Value::Object(section) = value else {
233            continue;
234        };
235        out.push(b'[');
236        out.extend_from_slice(key.as_bytes());
237        out.push(b']');
238        out.push(b'\n');
239        for (skey, svalue) in section.iter() {
240            out.extend_from_slice(skey.as_bytes());
241            out.extend_from_slice(b" = ");
242            emit_value(
243                out,
244                &scalar_text(svalue)?,
245                matches!(svalue, Value::String(_)),
246            );
247            out.push(b'\n');
248        }
249        out.push(b'\n');
250    }
251    Ok(())
252}
253
254// ---------------------------------------------------------------------------
255// Decoder (parse into a Value tree)
256// ---------------------------------------------------------------------------
257
258/// Strip an INI comment (`;` or `#` outside a quoted region).
259fn strip_comment(line: &[u8]) -> &[u8] {
260    let mut in_single = false;
261    let mut in_double = false;
262    for (i, &b) in line.iter().enumerate() {
263        match b {
264            b'\'' if !in_double => in_single = !in_single,
265            b'"' if !in_single => in_double = !in_double,
266            b';' | b'#' if !in_single && !in_double => return &line[..i],
267            _ => {}
268        }
269    }
270    line
271}
272
273fn trim(mut line: &[u8]) -> &[u8] {
274    while let Some((first, rest)) = line.split_first() {
275        if first.is_ascii_whitespace() {
276            line = rest;
277        } else {
278            break;
279        }
280    }
281    while let Some((last, rest)) = line.split_last() {
282        if last.is_ascii_whitespace() {
283            line = rest;
284        } else {
285            break;
286        }
287    }
288    line
289}
290
291/// Parse a quoted string value, returning the raw text.
292///
293/// `'...'` is literal; `"..."` supports `\\`, `\"`, `\n`, `\t`, `\r`.
294fn parse_quoted(raw: &[u8], quote: u8) -> Result<String> {
295    let mut out = Vec::with_capacity(raw.len());
296    if quote == b'\'' {
297        return String::from_utf8(raw.to_vec())
298            .map_err(|_| Error::custom("ini: invalid utf-8 in literal string"));
299    }
300    let mut i = 0;
301    while i < raw.len() {
302        let b = raw[i];
303        if b == b'\\' {
304            i += 1;
305            let esc = *raw
306                .get(i)
307                .ok_or_else(|| Error::custom("ini: truncated escape"))?;
308            match esc {
309                b'\\' => out.push(b'\\'),
310                b'"' => out.push(b'"'),
311                b'n' => out.push(b'\n'),
312                b't' => out.push(b'\t'),
313                b'r' => out.push(b'\r'),
314                other => {
315                    return Err(Error::custom(alloc::format!(
316                        "ini: unsupported escape \\{}",
317                        other as char
318                    )));
319                }
320            }
321            i += 1;
322        } else {
323            out.push(b);
324            i += 1;
325        }
326    }
327    String::from_utf8(out).map_err(|_| Error::custom("ini: invalid utf-8 in value"))
328}
329
330/// Classify an unquoted INI value into a typed [`Value`].
331///
332/// INI is a stringly-typed format, but the JSON data model is typed, so
333/// round-tripping `nextjson`-produced INI (where numbers/booleans are
334/// written as their plain text form) requires guessing the type back.
335/// Unquoted values are therefore parsed as `true`/`false`, then as an
336/// integer, then as a float, and fall back to a string otherwise. Quoted
337/// values always stay strings.
338fn classify_value(raw: &[u8]) -> Value {
339    let Ok(text) = core::str::from_utf8(raw) else {
340        return Value::String(String::from_utf8_lossy(raw).into_owned());
341    };
342    match text {
343        "true" => return Value::Bool(true),
344        "false" => return Value::Bool(false),
345        _ => {}
346    }
347    // Integral value? (kept within the JSON number model.)
348    if let Ok(n) = parse_ini_int(text) {
349        return Value::Number(n);
350    }
351    // Float value?
352    if text.contains(['.', 'e', 'E'])
353        && text
354            .bytes()
355            .all(|b| b.is_ascii_digit() || matches!(b, b'.' | b'e' | b'E' | b'+' | b'-'))
356    {
357        if let Some(v) = tree::parse_float(text) {
358            return Value::Number(Number::F64(v));
359        }
360    }
361    Value::String(text.to_string())
362}
363
364/// Parse a decimal integer (optionally signed) into a [`Number`].
365fn parse_ini_int(text: &str) -> Result<Number> {
366    if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
367        return Err(Error::custom("ini: not an integer"));
368    }
369    let (negative, body) = match text.strip_prefix('-') {
370        Some(rest) => (true, rest),
371        None => (false, text),
372    };
373    if body.is_empty() || !body.bytes().all(|b| b.is_ascii_digit()) {
374        return Err(Error::custom("ini: not an integer"));
375    }
376    let magnitude: u128 = body
377        .parse()
378        .map_err(|_| Error::custom("ini: integer overflow"))?;
379    if negative {
380        if magnitude == (i64::MAX as u128) + 1 {
381            Ok(Number::I64(i64::MIN))
382        } else if magnitude <= i64::MAX as u128 {
383            Ok(Number::I64(-(magnitude as i64)))
384        } else {
385            Ok(Number::I128(-(magnitude as i128)))
386        }
387    } else if magnitude <= u64::MAX as u128 {
388        Ok(Number::U64(magnitude as u64))
389    } else {
390        Ok(Number::U128(magnitude))
391    }
392}
393
394/// Parse one key/value line into `(key, value)`.
395fn parse_pair(line: &[u8]) -> Result<(String, Value)> {
396    let eq = line
397        .iter()
398        .position(|b| *b == b'=')
399        .ok_or_else(|| Error::custom("ini: expected `key = value`"))?;
400    let key = trim(&line[..eq]);
401    let key = core::str::from_utf8(key).map_err(|_| Error::custom("ini: invalid utf-8 in key"))?;
402    let key = key.trim().to_string();
403    if key.is_empty() {
404        return Err(Error::custom("ini: empty key"));
405    }
406    let value = trim(&line[eq + 1..]);
407    // A quoted value is always a string; an unquoted value is type-guessed.
408    let value = if value.len() >= 2
409        && ((value[0] == b'"' && value[value.len() - 1] == b'"')
410            || (value[0] == b'\'' && value[value.len() - 1] == b'\''))
411    {
412        Value::String(parse_quoted(&value[1..value.len() - 1], value[0])?)
413    } else {
414        classify_value(value)
415    };
416    Ok((key, value))
417}
418
419/// Parse an INI document into a [`Value`] tree.
420///
421/// Repeated sections merge; repeated keys within a section use the last
422/// value (common INI semantics).
423fn parse_ini(input: &[u8]) -> Result<Value> {
424    let mut root: Map = Map::new();
425    let mut current_name: Option<String> = None;
426    let mut current: Map = Map::new();
427    let mut lines = input.split(|b| *b == b'\n');
428    for line in lines.by_ref() {
429        let line = strip_comment(line);
430        let line = trim(line);
431        if line.is_empty() {
432            continue;
433        }
434        if line[0] == b'[' {
435            // Commit the previous section, then start (or reopen) a section.
436            if let Some(name) = current_name.take() {
437                match root.get_mut(&name) {
438                    Some(Value::Object(existing)) => {
439                        for (k, v) in core::mem::take(&mut current) {
440                            existing.insert(k, v);
441                        }
442                    }
443                    _ => {
444                        root.insert(name, Value::Object(core::mem::take(&mut current)));
445                    }
446                }
447            }
448            let close = line
449                .iter()
450                .rposition(|b| *b == b']')
451                .ok_or_else(|| Error::custom("ini: unterminated section header"))?;
452            let name = trim(&line[1..close]);
453            let name = core::str::from_utf8(name)
454                .map_err(|_| Error::custom("ini: invalid utf-8 in section"))?;
455            let name = name.trim();
456            if name.is_empty() {
457                return Err(Error::custom("ini: empty section name"));
458            }
459            current_name = Some(name.to_string());
460            continue;
461        }
462        let (key, value) = parse_pair(line)?;
463        match &current_name {
464            None => {
465                if root.insert(key, value).is_some() {
466                    return Err(Error::custom("ini: duplicate key in global section"));
467                }
468            }
469            Some(_) => {
470                if current.insert(key, value).is_some() {
471                    return Err(Error::custom("ini: duplicate key in section"));
472                }
473            }
474        }
475    }
476    // Commit the final section.
477    if let Some(name) = current_name {
478        match root.get_mut(&name) {
479            Some(Value::Object(existing)) => {
480                for (k, v) in current {
481                    existing.insert(k, v);
482                }
483            }
484            _ => {
485                root.insert(name, Value::Object(current));
486            }
487        }
488    }
489    Ok(Value::Object(root))
490}