1use std::{cell::Cell, fmt::Write};
25use toml::{Table, Value, ser::Error};
26
27pub fn write_table(table: &Table, out: &mut String) -> Result<(), Error> {
32 emit_table(out, table, &State::End)
33}
34
35pub fn reorder_value(value: &Value) -> Value {
48 match value {
49 Value::String(_)
50 | Value::Integer(_)
51 | Value::Float(_)
52 | Value::Boolean(_)
53 | Value::Datetime(_) => value.clone(),
54 Value::Array(array) => Value::Array(array.iter().map(reorder_value).collect()),
55 Value::Table(table) => {
56 let mut reordered = Table::new();
57 for (key, value) in table {
58 if !value.is_table() && !is_array_of_tables(value) {
59 reordered.insert(key.clone(), reorder_value(value));
60 }
61 }
62 for (key, value) in table {
63 if is_array_of_tables(value) {
64 reordered.insert(key.clone(), reorder_value(value));
65 }
66 }
67 for (key, value) in table {
68 if value.is_table() {
69 reordered.insert(key.clone(), reorder_value(value));
70 }
71 }
72 Value::Table(reordered)
73 }
74 }
75}
76
77fn is_array_of_tables(value: &Value) -> bool {
79 match value {
80 Value::Array(array) => array.iter().any(Value::is_table),
81 Value::String(_)
82 | Value::Integer(_)
83 | Value::Float(_)
84 | Value::Boolean(_)
85 | Value::Datetime(_)
86 | Value::Table(_) => false,
87 }
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91enum ArrayState {
92 Started,
93 StartedAsATable,
94}
95
96#[derive(Clone, Copy, Debug)]
97enum State<'a> {
98 End,
99 Table {
100 key: &'a str,
101 parent: &'a State<'a>,
102 first: &'a Cell<bool>,
103 table_emitted: &'a Cell<bool>,
104 },
105 Array {
106 parent: &'a State<'a>,
107 first: &'a Cell<bool>,
108 type_: &'a Cell<Option<ArrayState>>,
109 },
110}
111
112fn value_after_table() -> Error {
113 serde::ser::Error::custom("values must be emitted before tables")
114}
115
116fn emit_value(out: &mut String, value: &Value, state: &State<'_>) -> Result<(), Error> {
117 match value {
118 Value::String(s) => {
119 emit_key(out, state, ArrayState::Started)?;
120 emit_str(out, s, false);
121 newline_if_table(out, state);
122 Ok(())
123 }
124 Value::Integer(i) => display(out, i, state),
125 Value::Float(f) => {
126 emit_key(out, state, ArrayState::Started)?;
127 emit_float(out, *f);
128 newline_if_table(out, state);
129 Ok(())
130 }
131 Value::Boolean(b) => display(out, b, state),
132 Value::Datetime(dt) => display(out, dt, state),
144 Value::Array(array) => emit_array(out, array, state),
145 Value::Table(table) => emit_table(out, table, state),
146 }
147}
148
149fn display(
150 out: &mut String,
151 value: impl std::fmt::Display,
152 state: &State<'_>,
153) -> Result<(), Error> {
154 emit_key(out, state, ArrayState::Started)?;
155 write!(out, "{value}").expect("writing to a String cannot fail");
156 newline_if_table(out, state);
157 Ok(())
158}
159
160fn emit_float(out: &mut String, v: f64) {
161 match (v.is_sign_negative(), v.is_nan(), v == 0.0) {
162 (true, true, _) => out.push_str("-nan"),
163 (false, true, _) => out.push_str("nan"),
164 (true, false, true) => out.push_str("-0.0"),
165 (false, false, true) => out.push_str("0.0"),
166 (_, false, false) => {
167 write!(out, "{v}").expect("writing to a String cannot fail");
168 if v % 1.0 == 0.0 {
169 out.push_str(".0");
170 }
171 }
172 }
173}
174
175fn newline_if_table(out: &mut String, state: &State<'_>) {
176 match state {
177 State::Table { .. } => out.push('\n'),
178 State::End | State::Array { .. } => {}
179 }
180}
181
182fn emit_array(out: &mut String, array: &[Value], state: &State<'_>) -> Result<(), Error> {
183 array_type(state, ArrayState::Started);
184 let first = Cell::new(true);
185 let type_ = Cell::new(None);
186 for element in array {
187 emit_value(
188 out,
189 element,
190 &State::Array {
191 parent: state,
192 first: &first,
193 type_: &type_,
194 },
195 )?;
196 first.set(false);
197 }
198
199 match type_.get() {
200 Some(ArrayState::StartedAsATable) => return Ok(()),
201 Some(ArrayState::Started) => out.push(']'),
202 None => {
203 emit_key(out, state, ArrayState::Started)?;
204 out.push_str("[]");
205 }
206 }
207 newline_if_table(out, state);
208 Ok(())
209}
210
211fn emit_table(out: &mut String, table: &Table, state: &State<'_>) -> Result<(), Error> {
212 array_type(state, ArrayState::StartedAsATable);
213 let first = Cell::new(true);
214 let table_emitted = Cell::new(false);
215 for (key, value) in table {
216 emit_value(
217 out,
218 value,
219 &State::Table {
220 key,
221 parent: state,
222 first: &first,
223 table_emitted: &table_emitted,
224 },
225 )?;
226 first.set(false);
227 }
228
229 if first.get() {
230 emit_table_header(out, state);
231 }
232 Ok(())
233}
234
235fn emit_key(out: &mut String, state: &State<'_>, type_: ArrayState) -> Result<(), Error> {
236 array_type(state, type_);
237 emit_key_inner(out, state)
238}
239
240fn emit_key_inner(out: &mut String, state: &State<'_>) -> Result<(), Error> {
241 match *state {
242 State::End => Ok(()),
243 State::Array {
244 parent,
245 first,
246 type_,
247 } => {
248 assert!(
249 type_.get().is_some(),
250 "array_type is always called before emit_key_inner"
251 );
252 if first.get() {
253 emit_key_inner(out, parent)?;
254 }
255 if first.get() {
256 out.push('[');
257 } else {
258 out.push_str(", ");
259 }
260 Ok(())
261 }
262 State::Table {
263 key,
264 parent,
265 first,
266 table_emitted,
267 } => {
268 if table_emitted.get() {
269 return Err(value_after_table());
270 }
271 if first.get() {
272 emit_table_header(out, parent);
273 first.set(false);
274 }
275 escape_key(out, key);
276 out.push_str(" = ");
277 Ok(())
278 }
279 }
280}
281
282fn array_type(state: &State<'_>, type_: ArrayState) {
283 if let State::Array { type_: prev, .. } = state
284 && prev.get().is_none()
285 {
286 prev.set(Some(type_));
287 }
288}
289
290fn escape_key(out: &mut String, key: &str) {
291 let bare = !key.is_empty()
292 && key
293 .chars()
294 .all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_'));
295 if bare {
296 out.push_str(key);
297 } else {
298 emit_str(out, key, true);
299 }
300}
301
302#[derive(Clone, Copy, Debug, Eq, PartialEq)]
303enum StrType {
304 NewlineTriple,
305 OnelineTriple,
306 OnelineSingle,
307}
308
309enum StrRepr {
310 Literal(String, StrType),
312 Std(StrType),
314}
315
316fn str_repr(value: &str) -> StrRepr {
317 let mut out = String::with_capacity(value.len() * 2);
318 let mut ty = StrType::OnelineSingle;
319 let mut max_found_singles = 0;
320 let mut found_singles = 0;
321 let mut can_be_pretty = true;
322
323 for ch in value.chars() {
324 if can_be_pretty {
325 if ch == '\'' {
326 found_singles += 1;
327 if found_singles >= 3 {
328 can_be_pretty = false;
329 }
330 } else {
331 if found_singles > max_found_singles {
332 max_found_singles = found_singles;
333 }
334 found_singles = 0
335 }
336 match ch {
337 '\t' => {}
338 '\n' => ty = StrType::NewlineTriple,
339 c if c <= '\u{1f}' || c == '\u{7f}' => can_be_pretty = false,
340 _ => {}
341 }
342 out.push(ch);
343 } else if ch == '\n' {
344 ty = StrType::NewlineTriple;
345 }
346 }
347 if can_be_pretty && found_singles > 0 && value.ends_with('\'') {
348 can_be_pretty = false;
349 }
350 if !can_be_pretty {
351 debug_assert!(ty != StrType::OnelineTriple);
352 return StrRepr::Std(ty);
353 }
354 if found_singles > max_found_singles {
355 max_found_singles = found_singles;
356 }
357 debug_assert!(max_found_singles < 3);
358 if ty == StrType::OnelineSingle && max_found_singles >= 1 {
359 ty = StrType::OnelineTriple;
360 }
361 StrRepr::Literal(out, ty)
362}
363
364fn emit_str(out: &mut String, value: &str, is_key: bool) {
365 let repr = if is_key {
366 StrRepr::Std(StrType::OnelineSingle)
367 } else {
368 str_repr(value)
369 };
370 match repr {
371 StrRepr::Literal(literal, ty) => {
372 match ty {
373 StrType::NewlineTriple => out.push_str("'''\n"),
374 StrType::OnelineTriple => out.push_str("'''"),
375 StrType::OnelineSingle => out.push('\''),
376 }
377 out.push_str(&literal);
378 match ty {
379 StrType::OnelineSingle => out.push('\''),
380 StrType::NewlineTriple | StrType::OnelineTriple => out.push_str("'''"),
381 }
382 }
383 StrRepr::Std(ty) => {
384 match ty {
385 StrType::NewlineTriple => out.push_str("\"\"\"\n"),
386 StrType::OnelineSingle | StrType::OnelineTriple => out.push('"'),
387 }
388 for ch in value.chars() {
389 match ch {
390 '\u{8}' => out.push_str("\\b"),
391 '\u{9}' => out.push_str("\\t"),
392 '\u{a}' => match ty {
393 StrType::NewlineTriple => out.push('\n'),
394 StrType::OnelineSingle => out.push_str("\\n"),
395 StrType::OnelineTriple => {
396 unreachable!("newlines always produce NewlineTriple")
397 }
398 },
399 '\u{c}' => out.push_str("\\f"),
400 '\u{d}' => out.push_str("\\r"),
401 '\u{22}' => out.push_str("\\\""),
402 '\u{5c}' => out.push_str("\\\\"),
403 c if c <= '\u{1f}' || c == '\u{7f}' => {
404 write!(out, "\\u{:04X}", ch as u32)
405 .expect("writing to a String cannot fail");
406 }
407 ch => out.push(ch),
408 }
409 }
410 match ty {
411 StrType::NewlineTriple => out.push_str("\"\"\""),
412 StrType::OnelineSingle | StrType::OnelineTriple => out.push('"'),
413 }
414 }
415 }
416}
417
418fn emit_table_header(out: &mut String, state: &State<'_>) {
419 let array_of_tables = match state {
420 State::End => return,
421 State::Array { .. } => true,
422 State::Table { .. } => false,
423 };
424
425 let mut p = state;
428 if let State::Array { first, parent, .. } = *state
429 && first.get()
430 {
431 p = parent;
432 }
433 while let State::Table { first, parent, .. } = *p {
434 p = parent;
435 if !first.get() {
436 break;
437 }
438 if let State::Array {
439 parent: State::Table { .. },
440 ..
441 } = *parent
442 {
443 emit_table_header(out, parent);
444 break;
445 }
446 }
447
448 match *state {
449 State::Table { first, .. } => {
450 if !first.get() {
451 out.push('\n');
452 }
453 }
454 State::Array { parent, first, .. } => {
455 if !first.get() {
456 out.push('\n');
457 } else if let State::Table { first, .. } = *parent
458 && !first.get()
459 {
460 out.push('\n');
461 }
462 }
463 State::End => {}
464 }
465 out.push('[');
466 if array_of_tables {
467 out.push('[');
468 }
469 _ = emit_key_part(out, state);
470 if array_of_tables {
471 out.push(']');
472 }
473 out.push_str("]\n");
474}
475
476#[must_use]
479fn emit_key_part(out: &mut String, state: &State<'_>) -> bool {
480 match *state {
481 State::Array { parent, .. } => emit_key_part(out, parent),
482 State::End => true,
483 State::Table {
484 key,
485 parent,
486 table_emitted,
487 ..
488 } => {
489 table_emitted.set(true);
490 let first = emit_key_part(out, parent);
491 if !first {
492 out.push('.');
493 }
494 escape_key(out, key);
495 false
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 fn write(input: &str) -> Result<String, Error> {
505 let table: Table = toml::from_str(input).expect("valid TOML input");
506 let mut out = String::new();
507 write_table(&table, &mut out)?;
508 Ok(out)
509 }
510
511 #[test]
514 fn summary_shape() {
515 let input = r#"
516hakari-package = "workspace-hack"
517resolver = "2"
518output-single-feature = true
519platforms = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]
520empty = []
521[traversal-excludes]
522workspace-members = ["a"]
523[[traversal-excludes.ids]]
524name = "cargo-compare"
525version = "0.1.0"
526workspace-path = "internal-tools/cargo-compare"
527[[traversal-excludes.ids]]
528name = "serde"
529version = "1.0.0"
530crates-io = true
531[[traversal-excludes.third-party]]
532name = "quote"
533version = "1"
534crates-io = true
535[final-excludes]
536[registries.alt]
537index = "https://example.com/alt"
538[registries."my.registry"]
539index = "https://example.com/index"
540"#;
541 let expected = "\
542hakari-package = 'workspace-hack'
543resolver = '2'
544output-single-feature = true
545platforms = ['x86_64-unknown-linux-gnu', 'aarch64-apple-darwin']
546empty = []
547
548[traversal-excludes]
549workspace-members = ['a']
550
551[[traversal-excludes.ids]]
552name = 'cargo-compare'
553version = '0.1.0'
554workspace-path = 'internal-tools/cargo-compare'
555
556[[traversal-excludes.ids]]
557name = 'serde'
558version = '1.0.0'
559crates-io = true
560
561[[traversal-excludes.third-party]]
562name = 'quote'
563version = '1'
564crates-io = true
565
566[final-excludes]
567[registries.alt]
568index = 'https://example.com/alt'
569
570[registries.\"my.registry\"]
571index = 'https://example.com/index'
572";
573 assert_eq!(write(input).unwrap(), expected);
574 }
575
576 #[test]
577 fn strings() {
578 let input = r#"
579plain = "abc"
580apostrophe = "it's"
581ends-with-apostrophe = "ends'"
582triple = "'''"
583double-quotes = "say \"hi\""
584escapes = "tab\there\u0001\u007f"
585multiline = "a\nb"
586multiline-apostrophe = "a'\nb"
587multiline-control = "a\r\nb"
588backslash = "C:\\dir"
589"#;
590 let expected = "\
591plain = 'abc'
592apostrophe = '''it's'''
593ends-with-apostrophe = \"ends'\"
594triple = \"'''\"
595double-quotes = 'say \"hi\"'
596escapes = \"tab\\there\\u0001\\u007F\"
597multiline = '''
598a
599b'''
600multiline-apostrophe = '''
601a'
602b'''
603multiline-control = \"\"\"
604a\\r
605b\"\"\"
606backslash = 'C:\\dir'
607";
608 assert_eq!(write(input).unwrap(), expected);
609 }
610
611 #[test]
612 fn scalars_and_nesting() {
613 let input = r#"
614int = -42
615float = 1.0
616float2 = 2.5
617yes = false
618nested = [[1, 2], []]
619[a.b.c]
620x = 1
621[[aot]]
622[[aot]]
623y = 2
624[aot.sub]
625z = 3
626"#;
627 let expected = "\
628int = -42
629float = 1.0
630float2 = 2.5
631yes = false
632nested = [[1, 2], []]
633[a.b.c]
634x = 1
635
636[[aot]]
637
638[[aot]]
639y = 2
640
641[aot.sub]
642z = 3
643";
644 assert_eq!(write(input).unwrap(), expected);
645 }
646
647 #[test]
648 fn reorder_value_matches_toml_05() {
649 let input = r#"
650plain = [1, 2]
651v = true
652[t]
653x = 1
654[[aot]]
655[[aot]]
656sub = { inner = { y = 1 }, z = 2 }
657w = 3
658"#;
659 let table: Table = toml::from_str(input).expect("valid TOML input");
660 let reordered = match reorder_value(&Value::Table(table)) {
661 Value::Table(table) => table,
662 other => panic!("reordered a table into {other:?}"),
663 };
664 let keys: Vec<_> = reordered.keys().map(String::as_str).collect();
665 assert_eq!(keys, ["plain", "v", "aot", "t"]);
666
667 let second = reordered["aot"][1]
668 .as_table()
669 .expect("array of tables element is a table");
670 let keys: Vec<_> = second.keys().map(String::as_str).collect();
671 assert_eq!(keys, ["w", "sub"], "array elements are reordered");
672 let sub = second["sub"].as_table().expect("sub is a table");
673 let keys: Vec<_> = sub.keys().map(String::as_str).collect();
674 assert_eq!(keys, ["z", "inner"], "nested tables are reordered");
675
676 let mut out = String::new();
677 write_table(&reordered, &mut out).expect("reordered table serializes");
678 assert_eq!(
679 out,
680 "\
681plain = [1, 2]
682v = true
683
684[[aot]]
685
686[[aot]]
687w = 3
688
689[aot.sub]
690z = 2
691
692[aot.sub.inner]
693y = 1
694
695[t]
696x = 1
697"
698 );
699 }
700
701 #[test]
702 fn value_after_table_is_an_error() {
703 let mut table = Table::new();
704 table.insert("t".to_owned(), Value::Table(Table::new()));
705 table.insert("v".to_owned(), Value::Integer(1));
706 let mut out = String::new();
707 let err = write_table(&table, &mut out).unwrap_err();
708 assert!(
709 err.to_string()
710 .contains("values must be emitted before tables"),
711 "unexpected error: {err}"
712 );
713 }
714}