Skip to main content

tuwunel_core/config/
regenerate.rs

1use std::{
2	cmp::Ordering,
3	fmt::Write as _,
4	iter::once,
5	path::{Path, PathBuf},
6	sync::LazyLock,
7};
8
9mod overlay;
10#[cfg(test)]
11mod tests;
12mod tree;
13mod write;
14
15use figment::{
16	Figment,
17	providers::{Data, Format as _, Toml},
18	value::{Dict, Value},
19};
20use itertools::Itertools as _;
21use link_section::TypedSection;
22use serde::Serialize as _;
23use smallvec::SmallVec;
24use toml::{Value as TomlValue, ser::ValueSerializer};
25use toml_writer::TomlWrite as _;
26
27use self::{
28	overlay::{filter_nonconfig_environment, is_file_value, validate_file_overlay},
29	tree::{
30		ConfigPath, Instance, PathPart, find_path, normalize_aliases, remove_path,
31		resolve_instances,
32	},
33	write::write_atomic,
34};
35use super::{Config, DEPRECATED_KEYS, Sources};
36use crate::{Err, Error, Result, err, implement, utils::BoolExt};
37
38const NEVER_EMIT: [&str; 2] = ["database_restore_backup", "force_migration"];
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub(super) enum FieldClass {
42	Documented,
43	Structural,
44	Hidden,
45	Forbidden,
46}
47
48#[derive(Clone, Copy, Debug)]
49pub(super) struct FieldSpec {
50	pub(super) name: &'static str,
51	pub(super) aliases: &'static [&'static str],
52	pub(super) example: &'static str,
53	pub(super) class: FieldClass,
54}
55
56#[derive(Clone, Copy, Debug)]
57pub(super) struct SectionSpec {
58	pub(super) section: &'static str,
59	pub(super) aliases: &'static [&'static str],
60	pub(super) example: &'static str,
61	pub(super) fields: &'static [FieldSpec],
62	pub(super) position: SourcePosition,
63}
64
65#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
66pub(super) struct SourcePosition {
67	pub(super) file: &'static str,
68	pub(super) line: u32,
69	pub(super) column: u32,
70}
71
72/// Selects how an existing configuration is regenerated.
73///
74/// The output path is optional so a single input can use its adjacent `.new`
75/// destination. The remaining flags control replacement and value selection.
76#[derive(Clone, Copy, Debug, Default)]
77pub struct RegenerateOptions<'a> {
78	/// Selects the output path.
79	///
80	/// When omitted, regeneration writes beside its sole input with a `.new`
81	/// suffix. Layered inputs require an explicit destination.
82	pub output: Option<&'a Path>,
83
84	/// Allows replacement of an existing output.
85	///
86	/// The previous contents are retained beside the output with a `.bak`
87	/// suffix before the atomic replacement is installed.
88	pub force: bool,
89
90	/// Materializes configuration values supplied through the environment.
91	///
92	/// The default keeps file values active and annotates environment overrides
93	/// without copying them into the regenerated document.
94	pub include_env: bool,
95
96	/// Comments out deprecated and unknown residue keys.
97	///
98	/// Hidden but valid configuration remains active. Forbidden migration
99	/// controls are always removed regardless of this setting.
100	pub strip_unknown: bool,
101}
102
103/// Selects whether an existing output may be replaced.
104///
105/// Replacement preserves the previous file beside the output as a `.bak`
106/// backup before installing the regenerated contents.
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub enum Overwrite {
109	/// Refuses to replace an existing output.
110	///
111	/// A preexisting destination is left untouched and causes an error.
112	Deny,
113
114	/// Replaces an existing output after retaining a backup.
115	///
116	/// The previous regular file is preserved at the adjacent backup path.
117	Allow,
118}
119
120/// Summarizes a completed configuration regeneration.
121///
122/// Counts distinguish input layers, schema fields, and preserved residue.
123/// Intentionally removed migration controls are available through
124/// [`Self::dropped_keys`].
125#[derive(Debug)]
126pub struct RegenerationSummary {
127	output: PathBuf,
128	input_count: usize,
129	configured: usize,
130	residue: usize,
131	dropped: [bool; NEVER_EMIT.len()],
132}
133
134#[derive(Default)]
135struct RenderStats {
136	configured: usize,
137	residue: usize,
138	dropped: [bool; NEVER_EMIT.len()],
139}
140
141struct RenderContext<'a> {
142	environment: Option<(&'a Figment, &'a Dict)>,
143	strip_unknown: bool,
144	expected: &'a mut Dict,
145	stats: &'a mut RenderStats,
146}
147
148#[derive(Clone, Copy)]
149enum ResidueKind {
150	Deprecated,
151	Hidden,
152	Unknown,
153}
154
155#[derive(Clone, Copy)]
156enum ResidueDisposition {
157	Active,
158	Commented,
159}
160
161#[link_section::section(typed)]
162pub(super) static REGISTERED_SECTIONS: TypedSection<SectionSpec>;
163
164type OrderedSections = SmallVec<[&'static SectionSpec; 16]>;
165
166static ORDERED_SECTIONS: LazyLock<OrderedSections> = LazyLock::new(ordered_sections);
167
168#[inline]
169fn schema() -> &'static [&'static SectionSpec] { ORDERED_SECTIONS.as_slice() }
170
171fn ordered_sections() -> OrderedSections {
172	let mut sections = REGISTERED_SECTIONS
173		.as_slice()
174		.iter()
175		.collect::<OrderedSections>();
176
177	// Linker-section order is unspecified, so restore source declaration order.
178	sections.sort_unstable_by_key(|section| section.position);
179	validate_schema(&sections);
180
181	sections
182}
183
184fn validate_schema(sections: &[&SectionSpec]) {
185	let first = sections
186		.first()
187		.expect("configuration schema must contain the global section");
188
189	assert!(
190		sections
191			.iter()
192			.all(|section| section.position.file == first.position.file),
193		"configuration sections must be declared in one source file",
194	);
195
196	assert_eq!(first.section, "global", "global must be the first configuration section");
197
198	for pair in sections.windows(2) {
199		assert_ne!(
200			pair[0].position, pair[1].position,
201			"configuration sections share a source location"
202		);
203	}
204
205	for (index, section) in sections.iter().enumerate() {
206		assert!(
207			sections[..index]
208				.iter()
209				.all(|previous| previous.section != section.section),
210			"configuration section {:?} is registered more than once",
211			section.section,
212		);
213	}
214}
215
216/// Renders the example configuration carried by this binary.
217///
218/// The returned document is generated from the same runtime schema that
219/// produces the checked-in example file.
220pub fn example_config() -> Result<String> {
221	let values = Dict::new();
222	let mut expected = Dict::new();
223	let mut stats = RenderStats::default();
224	let rendered = {
225		let mut context = RenderContext {
226			environment: None,
227			strip_unknown: false,
228			expected: &mut expected,
229			stats: &mut stats,
230		};
231
232		render_schema(&values, &mut context)?
233	};
234
235	Ok(rendered)
236}
237
238/// Writes the example configuration to a filesystem path.
239///
240/// New files use private permissions. [`Overwrite::Allow`] retains an existing
241/// file beside the replacement with a `.bak` suffix.
242pub fn write_example_config(path: &Path, overwrite: Overwrite) -> Result {
243	let rendered = example_config()?;
244
245	write_atomic(path, rendered.as_bytes(), matches!(overwrite, Overwrite::Allow))
246}
247
248/// Regenerates the file-backed configuration retained by `sources`.
249///
250/// Command-line overrides are never consulted. Environment values are either
251/// annotated or explicitly materialized according to `options`.
252pub fn regenerate_config(
253	sources: &Sources,
254	options: RegenerateOptions<'_>,
255) -> Result<RegenerationSummary> {
256	let paths = sources.file_paths().unique().collect_vec();
257	let input_count = paths.len();
258
259	if paths.is_empty() {
260		return Err!("Configuration regeneration requires at least one input file.");
261	}
262
263	if paths.len() > 1 && options.output.is_none() {
264		return Err!(
265			"Layered configuration files require an explicit output path because they are \
266			 collapsed into one document."
267		);
268	}
269
270	let output = options
271		.output
272		.map(Path::to_path_buf)
273		.unwrap_or_else(|| adjacent_new_path(&paths[0]));
274
275	let files = Config::load_files(paths.iter().map(PathBuf::as_path))?;
276	validate_file_overlay(&files)?;
277
278	let file_values = options
279		.include_env
280		.is_false()
281		.then(|| files.extract::<Dict>())
282		.transpose()?;
283
284	let effective = Config::merge_environment(files);
285	Config::new(&effective)?;
286
287	let mut values = match file_values {
288		| Some(values) => values,
289		| None => {
290			let mut values = effective.extract::<Dict>()?;
291
292			filter_nonconfig_environment(&effective, &mut values)?;
293			values
294		},
295	};
296
297	normalize_aliases(&mut values, schema());
298
299	let effective_values = options
300		.include_env
301		.is_false()
302		.then(|| effective.extract::<Dict>())
303		.transpose()?
304		.map(|mut values| {
305			normalize_aliases(&mut values, schema());
306
307			values
308		});
309
310	let mut expected = values.clone();
311	let mut stats = RenderStats::default();
312	let rendered = {
313		let mut context = RenderContext {
314			environment: effective_values
315				.as_ref()
316				.map(|values| (&effective, values)),
317			strip_unknown: options.strip_unknown,
318			expected: &mut expected,
319			stats: &mut stats,
320		};
321
322		render_schema(&values, &mut context)?
323	};
324
325	verify_rendered(&rendered, &expected)?;
326	write_atomic(&output, rendered.as_bytes(), options.force)?;
327	verify_file(&output, &expected).map_err(|error| {
328		err!(
329			"Configuration was written to {} but post-write verification failed. Inspect the \
330			 written file and restore its adjacent `.bak` backup if present before retrying: \
331			 {error}",
332			output.display(),
333		)
334	})?;
335
336	let summary = RegenerationSummary {
337		output,
338		input_count,
339		configured: stats.configured,
340		residue: stats.residue,
341		dropped: stats.dropped,
342	};
343
344	Ok(summary)
345}
346
347/// Returns the path written by regeneration.
348///
349/// The path is absolute or relative according to the caller's selected
350/// destination.
351#[implement(RegenerationSummary)]
352#[inline]
353#[must_use]
354pub fn output(&self) -> &Path { &self.output }
355
356/// Returns the number of unique input files collapsed into the output.
357///
358/// A value greater than one means the result combines layered configuration
359/// sources in their normal precedence order.
360#[implement(RegenerationSummary)]
361#[inline]
362#[must_use]
363pub fn input_count(&self) -> usize { self.input_count }
364
365/// Returns the number of configured schema fields written.
366///
367/// Residue keys are counted separately by [`Self::residue`].
368#[implement(RegenerationSummary)]
369#[inline]
370#[must_use]
371pub fn configured(&self) -> usize { self.configured }
372
373/// Returns the number of encountered hidden, deprecated, or unknown keys.
374///
375/// The count includes commented residue when stripping was requested.
376#[implement(RegenerationSummary)]
377#[inline]
378#[must_use]
379pub fn residue(&self) -> usize { self.residue }
380
381/// Iterates over migration controls intentionally removed from the output.
382///
383/// The iterator is empty when neither control appeared in the selected
384/// configuration values.
385#[implement(RegenerationSummary)]
386pub fn dropped_keys(&self) -> impl Iterator<Item = &'static str> + '_ {
387	NEVER_EMIT
388		.into_iter()
389		.zip(self.dropped)
390		.filter_map(|(key, dropped)| dropped.then_some(key))
391}
392
393fn render_schema(values: &Dict, context: &mut RenderContext<'_>) -> Result<String> {
394	let schema = schema();
395	let mut output = String::with_capacity(schema.iter().map(|spec| spec.example.len()).sum());
396
397	for &spec in schema {
398		let instances = resolve_instances(spec, values);
399		let dynamic = is_dynamic(spec.section);
400
401		for instance in &instances {
402			render_instance(&mut output, spec, instance, context)?;
403		}
404
405		let rendered_instance = instances.is_empty().is_false();
406
407		let rendered_instance = match context.environment {
408			| None => rendered_instance,
409			| Some(environment @ (_, environment_values)) => {
410				let environment_instances = resolve_instances(spec, environment_values);
411
412				environment_instances
413					.iter()
414					.filter(|candidate| {
415						instances
416							.iter()
417							.all(|instance| instance.path != candidate.path)
418					})
419					.try_fold(rendered_instance, |_, candidate| {
420						render_environment_instance(&mut output, spec, candidate, environment)?;
421
422						Ok::<_, Error>(true)
423					})?
424			},
425		};
426
427		if dynamic || !rendered_instance {
428			output.push_str(spec.example);
429		}
430	}
431
432	Ok(output)
433}
434
435fn render_instance(
436	output: &mut String,
437	spec: &SectionSpec,
438	instance: &Instance<'_>,
439	context: &mut RenderContext<'_>,
440) -> Result {
441	for line in spec.example.split_inclusive('\n') {
442		let body = line.strip_suffix('\n').unwrap_or(line);
443
444		if is_template_header(body, spec.section) {
445			write_active_header(output, spec.section, &instance.section);
446			if line.ends_with('\n') {
447				output.push('\n');
448			}
449
450			continue;
451		}
452
453		let Some(name) = assignment_name(body) else {
454			output.push_str(line);
455			continue;
456		};
457
458		let Some(field) = spec
459			.fields
460			.iter()
461			.find(|field| field.class == FieldClass::Documented && field.name == name)
462		else {
463			output.push_str(line);
464			continue;
465		};
466
467		let path = field_path(&instance.path, field.name);
468		annotate_environment(
469			output,
470			context.environment,
471			&path,
472			instance.values.get(field.name),
473		)?;
474
475		match instance.values.get(field.name) {
476			| None => output.push_str(line),
477			| Some(value) => {
478				write_assignment(output, field.name, value)?;
479				context.stats.configured = context.stats.configured.saturating_add(1);
480			},
481		}
482	}
483
484	render_unlisted_fields(output, spec, instance, context)
485}
486
487fn render_environment_instance(
488	output: &mut String,
489	spec: &SectionSpec,
490	instance: &Instance<'_>,
491	environment: (&Figment, &Dict),
492) -> Result {
493	for line in spec.example.split_inclusive('\n') {
494		let body = line.strip_suffix('\n').unwrap_or(line);
495
496		if is_template_header(body, spec.section) {
497			output.push('#');
498			write_active_header(output, spec.section, &instance.section);
499			if line.ends_with('\n') {
500				output.push('\n');
501			}
502
503			continue;
504		}
505
506		if let Some(field) = assignment_name(body).and_then(|name| {
507			spec.fields
508				.iter()
509				.find(|field| field.class == FieldClass::Documented && field.name == name)
510		}) {
511			let path = field_path(&instance.path, field.name);
512
513			annotate_environment(output, Some(environment), &path, None)?;
514		}
515
516		output.push_str(line);
517	}
518
519	for field in spec
520		.fields
521		.iter()
522		.filter(|field| field.class == FieldClass::Hidden)
523	{
524		let path = field_path(&instance.path, field.name);
525
526		render_hidden_environment(output, field, Some(environment), &path)?;
527	}
528
529	Ok(())
530}
531
532fn render_unlisted_fields(
533	output: &mut String,
534	spec: &SectionSpec,
535	instance: &Instance<'_>,
536	context: &mut RenderContext<'_>,
537) -> Result {
538	for field in spec.fields {
539		let Some(value) = instance.values.get(field.name) else {
540			if field.class == FieldClass::Hidden {
541				let path = field_path(&instance.path, field.name);
542
543				render_hidden_environment(output, field, context.environment, &path)?;
544			}
545
546			continue;
547		};
548
549		let path = field_path(&instance.path, field.name);
550
551		match field.class {
552			| FieldClass::Documented | FieldClass::Structural => {},
553			| FieldClass::Hidden => {
554				annotate_environment(output, context.environment, &path, Some(value))?;
555				render_residue(
556					output,
557					field.name,
558					value,
559					ResidueKind::Hidden,
560					ResidueDisposition::Active,
561				)?;
562
563				context.stats.residue = context.stats.residue.saturating_add(1);
564			},
565			| FieldClass::Forbidden => {
566				remove_path(context.expected, &path);
567				mark_dropped(context.stats, field.name);
568			},
569		}
570	}
571
572	for (name, value) in instance.values {
573		if spec.fields.iter().any(|field| field.name == name) {
574			continue;
575		}
576
577		let path = field_path(&instance.path, name);
578		let kind = is_deprecated(&path)
579			.then_some(ResidueKind::Deprecated)
580			.unwrap_or(ResidueKind::Unknown);
581
582		let disposition = if context.strip_unknown {
583			ResidueDisposition::Commented
584		} else {
585			ResidueDisposition::Active
586		};
587
588		annotate_environment(output, context.environment, &path, Some(value))?;
589		render_residue(output, name, value, kind, disposition)?;
590		context.stats.residue = context.stats.residue.saturating_add(1);
591
592		if context.strip_unknown {
593			remove_path(context.expected, &path);
594		}
595	}
596
597	Ok(())
598}
599
600fn render_hidden_environment(
601	output: &mut String,
602	field: &FieldSpec,
603	environment: Option<(&Figment, &Dict)>,
604	path: &[PathPart<'_>],
605) -> Result {
606	let Some((figment, value)) = environment_value(environment, path) else {
607		return Ok(());
608	};
609
610	writeln!(
611		output,
612		"\n# UNDOCUMENTED: `{}` is valid but omitted from the example configuration.",
613		field.name
614	)
615	.expect("written to configuration buffer");
616
617	output.push_str("# currently set by ");
618	write_environment_name(output, figment, value, path);
619	output.push_str(".\n");
620
621	match field.example {
622		| "" => writeln!(output, "#{} =", field.name),
623		| example => writeln!(output, "#{} = {example}", field.name),
624	}
625	.expect("written to configuration buffer");
626
627	Ok(())
628}
629
630fn render_residue(
631	output: &mut String,
632	name: &str,
633	value: &Value,
634	kind: ResidueKind,
635	disposition: ResidueDisposition,
636) -> Result {
637	match kind {
638		| ResidueKind::Hidden => writeln!(
639			output,
640			"\n# UNDOCUMENTED: `{name}` is valid but omitted from the example configuration."
641		)
642		.expect("written to configuration buffer"),
643		| ResidueKind::Deprecated => {
644			writeln!(
645				output,
646				"\n# DEPRECATED: `{name}` is no longer used by tuwunel and is ignored."
647			)
648			.expect("written to configuration buffer");
649
650			writeln!(output, "# Preserved from the previous configuration; it can be deleted.")
651				.expect("written to configuration buffer");
652		},
653		| ResidueKind::Unknown => {
654			writeln!(output, "\n# UNKNOWN: `{name}` is not a tuwunel configuration option.")
655				.expect("written to configuration buffer");
656
657			writeln!(output, "# Preserved from the previous configuration.")
658				.expect("written to configuration buffer");
659		},
660	}
661
662	if matches!(disposition, ResidueDisposition::Commented) {
663		output.push('#');
664	}
665
666	write_assignment(output, name, value)?;
667
668	Ok(())
669}
670
671fn annotate_environment(
672	output: &mut String,
673	environment: Option<(&Figment, &Dict)>,
674	path: &[PathPart<'_>],
675	file_value: Option<&Value>,
676) -> Result {
677	let Some((figment, value)) = environment_value(environment, path) else {
678		return Ok(());
679	};
680
681	let action = file_value
682		.is_some()
683		.then_some("currently overridden by")
684		.unwrap_or("currently set by");
685
686	write!(output, "# {action} ").expect("written to configuration buffer");
687	write_environment_name(output, figment, value, path);
688	output.push_str(".\n");
689
690	Ok(())
691}
692
693fn environment_value<'a>(
694	environment: Option<(&'a Figment, &'a Dict)>,
695	path: &[PathPart<'_>],
696) -> Option<(&'a Figment, &'a Value)> {
697	let (figment, values) = environment?;
698
699	find_path(values, path)
700		.filter(|value| !is_file_value(figment, value))
701		.map(|value| (figment, value))
702}
703
704fn write_assignment(output: &mut String, name: &str, value: &Value) -> Result {
705	output
706		.key(name)
707		.expect("written to configuration buffer");
708
709	output.push_str(" = ");
710	write_value(output, value)?;
711	output.push('\n');
712
713	Ok(())
714}
715
716fn write_value(output: &mut String, value: &Value) -> Result {
717	value
718		.serialize(ValueSerializer::new(output))
719		.map_err(|error| err!("Failed to serialize a configuration value: {error}"))?;
720
721	Ok(())
722}
723
724fn assignment_name(line: &str) -> Option<&str> {
725	let (name, _) = line.strip_prefix('#')?.split_once(" =")?;
726
727	name.chars()
728		.all(|character| {
729			character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
730		})
731		.then_some(name)
732}
733
734fn is_template_header(line: &str, section: &str) -> bool {
735	match section == "global" {
736		| true => line == "[global]",
737		| false => line
738			.strip_prefix("#[")
739			.and_then(|line| line.strip_suffix(']'))
740			.is_some_and(|line| line == section),
741	}
742}
743
744fn write_active_header(output: &mut String, section: &str, resolved: &str) {
745	if section.starts_with('[') {
746		write!(output, "[[{resolved}]]").expect("written to configuration buffer");
747	} else {
748		write!(output, "[{resolved}]").expect("written to configuration buffer");
749	}
750}
751
752fn is_dynamic(section: &str) -> bool { section.contains('<') || section.starts_with('[') }
753
754fn field_path<'a>(base: &ConfigPath<'a>, field: &'a str) -> ConfigPath<'a> {
755	base.iter()
756		.copied()
757		.chain(once(PathPart::Key(field)))
758		.collect()
759}
760
761fn is_deprecated(path: &[PathPart<'_>]) -> bool {
762	DEPRECATED_KEYS.iter().any(|deprecated| {
763		deprecated
764			.split('.')
765			.eq(path.iter().filter_map(|part| match part {
766				| PathPart::Key(key) => Some(*key),
767				| PathPart::Index(_) => None,
768			}))
769	})
770}
771
772fn mark_dropped(stats: &mut RenderStats, name: &str) {
773	if let Some(index) = NEVER_EMIT.iter().position(|key| *key == name) {
774		stats.dropped[index] = true;
775	}
776}
777
778fn write_environment_name(
779	output: &mut String,
780	figment: &Figment,
781	value: &Value,
782	path: &[PathPart<'_>],
783) {
784	let prefix = figment
785		.get_metadata(value.tag())
786		.and_then(|metadata| metadata.name.strip_prefix('`'))
787		.and_then(|name| name.split_once('`'))
788		.map(|(prefix, _)| prefix)
789		.unwrap_or("TUWUNEL_");
790
791	output.push_str(prefix);
792
793	for (index, key) in path
794		.iter()
795		.take_while(|part| matches!(part, PathPart::Key(_)))
796		.filter_map(|part| match part {
797			| PathPart::Key(key) => Some(*key),
798			| PathPart::Index(_) => None,
799		})
800		.enumerate()
801	{
802		if index > 0 {
803			output.push_str("__");
804		}
805
806		output.extend(
807			key.chars()
808				.map(|character| character.to_ascii_uppercase()),
809		);
810	}
811}
812
813fn verify_rendered(rendered: &str, expected: &Dict) -> Result {
814	let output = Figment::new().merge(Data::nested(Toml::string(rendered)));
815	let actual = output.extract::<Dict>()?;
816
817	if !toml_equivalent(&actual, expected)? {
818		return Err!("Regenerated configuration failed its semantic round-trip check.");
819	}
820
821	Ok(())
822}
823
824fn verify_file(path: &Path, expected: &Dict) -> Result {
825	let output = Config::load_files([path].into_iter())?;
826	let actual = output.extract::<Dict>()?;
827
828	if !toml_equivalent(&actual, expected)? {
829		return Err!("Written configuration failed its semantic round-trip check.");
830	}
831
832	Ok(())
833}
834
835fn toml_equivalent(left: &Dict, right: &Dict) -> Result<bool> {
836	let left = TomlValue::try_from(left)
837		.map_err(|error| err!("Failed to normalize regenerated values: {error}"))?;
838
839	let right = TomlValue::try_from(right)
840		.map_err(|error| err!("Failed to normalize expected values: {error}"))?;
841
842	Ok(toml_values_equivalent(&left, &right))
843}
844
845fn toml_values_equivalent(left: &TomlValue, right: &TomlValue) -> bool {
846	match (left, right) {
847		| (TomlValue::Float(left), TomlValue::Float(right)) =>
848			left.is_nan() && right.is_nan()
849				|| left
850					.partial_cmp(right)
851					.is_some_and(Ordering::is_eq),
852		| (TomlValue::Array(left), TomlValue::Array(right)) =>
853			left.len() == right.len()
854				&& left
855					.iter()
856					.zip(right)
857					.all(|(left, right)| toml_values_equivalent(left, right)),
858		| (TomlValue::Table(left), TomlValue::Table(right)) =>
859			left.len() == right.len()
860				&& left.iter().all(|(key, left)| {
861					right
862						.get(key)
863						.is_some_and(|right| toml_values_equivalent(left, right))
864				}),
865		| _ => left == right,
866	}
867}
868
869fn adjacent_new_path(input: &Path) -> PathBuf {
870	let mut output = input.as_os_str().to_owned();
871
872	output.push(".new");
873
874	output.into()
875}