1use std::borrow::Cow;
39use std::fmt;
40
41use rich::{Console, Style};
42
43use crate::event::theme_style;
44
45mod config;
46mod diff;
47mod dotenv;
48mod explorer;
49mod flatten;
50mod helpers;
51mod ini;
52mod json;
53mod redact;
54mod search;
55pub mod select;
56mod ser;
57mod table;
58#[cfg(feature = "jsonpath")]
59pub mod transform;
60
61#[cfg(feature = "toml")]
62mod toml_doc;
63#[cfg(feature = "xml")]
64mod xml;
65#[cfg(feature = "yaml")]
66mod yaml;
67
68pub use config::ConfigFileView;
69pub use diff::{diff, Change, ChangeKind, DiffView};
70pub use explorer::{Explorer, View};
71pub use flatten::{flatten, unflatten, FlatView, UnflattenError};
72pub use helpers::{
73 json, print_json, print_json_to, print_table, print_table_to, print_tree, print_tree_to, table,
74 tree,
75};
76pub use redact::SECRET_KEYS;
77pub use redact::{Redaction, Redactor};
78pub use search::{search, MatchKind, SearchMatch, SearchQuery, SearchResults};
79#[cfg(feature = "jsonpath")]
80pub use select::{JsonPath, JsonPathSelector};
81pub use select::{SelectError, Selector, SelectorBackend, Selectors};
82pub use table::{TableOptions, TableView};
83
84#[cfg(any(feature = "yaml", feature = "xml"))]
89pub(crate) const MAX_DEPTH: usize = 512;
90
91pub const DATA_STYLES: &[(&str, &str)] = &[
94 ("data.match", "bold reverse yellow"),
95 ("data.anchor", "dim cyan"),
96 ("data.alias", "dim cyan"),
97 ("data.attribute", "not italic yellow"),
98 ("data.index", "dim"),
99 ("data.comment", "dim"),
100 ("data.path", "dim"),
101 ("data.type", "dim italic"),
102 ("data.summary", "dim"),
103 ("data.null", "dim"),
104 ("data.datetime", "magenta"),
105 ("data.section", "bold"),
106 ("data.added", "green"),
107 ("data.removed", "red"),
108 ("data.changed", "yellow"),
109];
110
111const JSON_STYLES: &[(&str, &str)] = &[
113 ("json.key", "bold blue"),
114 ("json.str", "not bold not italic green"),
115 ("json.number", "bold not italic cyan"),
116 ("json.bool_true", "italic bright_green"),
117 ("json.bool_false", "italic bright_red"),
118 ("json.null", "italic magenta"),
119];
120
121pub(crate) fn style(console: &Console, key: &str) -> Style {
123 let fallback = DATA_STYLES
124 .iter()
125 .chain(JSON_STYLES)
126 .find(|(name, _)| *name == key)
127 .map_or("", |(_, spec)| *spec);
128 theme_style(console, key, fallback)
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
138pub struct Position {
139 pub line: usize,
140 pub column: usize,
141}
142
143impl Position {
144 pub fn new(line: usize, column: usize) -> Self {
145 Position { line, column }
146 }
147}
148
149impl fmt::Display for Position {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 write!(f, "{}:{}", self.line, self.column)
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
157pub enum XmlKind {
158 Element,
160 Attribute,
162 Text,
164}
165
166#[derive(Clone, Debug, Default, PartialEq)]
168pub struct Meta {
169 pub position: Option<Position>,
171 pub anchor: Option<String>,
173 pub alias: Option<String>,
176 pub comment: Option<String>,
178 pub xml: Option<XmlKind>,
180}
181
182#[derive(Clone, Debug, PartialEq)]
184pub enum Value {
185 Null,
186 Bool(bool),
187 Int(i64),
188 UInt(u64),
190 Float(f64),
191 String(String),
192 DateTime(String),
194 Seq(Vec<Node>),
195 Map(Vec<(String, Node)>),
197}
198
199#[derive(Clone, Debug, PartialEq)]
201pub struct Node {
202 pub value: Value,
203 pub meta: Meta,
204}
205
206impl From<Value> for Node {
207 fn from(value: Value) -> Self {
208 Node::new(value)
209 }
210}
211
212impl<'a> From<Node> for Cow<'a, Node> {
213 fn from(node: Node) -> Self {
214 Cow::Owned(node)
215 }
216}
217
218impl<'a> From<&'a Node> for Cow<'a, Node> {
219 fn from(node: &'a Node) -> Self {
220 Cow::Borrowed(node)
221 }
222}
223
224impl Node {
225 pub fn new(value: Value) -> Self {
227 Node {
228 value,
229 meta: Meta::default(),
230 }
231 }
232
233 pub fn with_meta(value: Value, meta: Meta) -> Self {
235 Node { value, meta }
236 }
237
238 pub fn at_position(mut self, position: Option<Position>) -> Self {
240 self.meta.position = position;
241 self
242 }
243
244 pub fn is_container(&self) -> bool {
246 matches!(self.value, Value::Seq(_) | Value::Map(_))
247 }
248
249 pub fn len(&self) -> usize {
251 match &self.value {
252 Value::Seq(items) => items.len(),
253 Value::Map(entries) => entries.len(),
254 _ => 0,
255 }
256 }
257
258 pub fn is_empty(&self) -> bool {
260 self.len() == 0
261 }
262
263 pub fn get(&self, key: &str) -> Option<&Node> {
265 match &self.value {
266 Value::Map(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v),
267 _ => None,
268 }
269 }
270
271 pub fn index(&self, index: usize) -> Option<&Node> {
273 match &self.value {
274 Value::Seq(items) => items.get(index),
275 _ => None,
276 }
277 }
278
279 pub fn at(&self, path: &Path) -> Option<&Node> {
281 path.segments()
282 .iter()
283 .try_fold(self, |node, segment| match segment {
284 PathSegment::Key(key) => node.get(key),
285 PathSegment::Index(index) => node.index(*index),
286 })
287 }
288
289 pub fn as_str(&self) -> Option<&str> {
291 match &self.value {
292 Value::String(s) | Value::DateTime(s) => Some(s),
293 _ => None,
294 }
295 }
296
297 pub fn type_name(&self) -> &'static str {
300 match &self.value {
301 Value::Null => "null",
302 Value::Bool(_) => "bool",
303 Value::Int(_) | Value::UInt(_) => "int",
304 Value::Float(_) => "float",
305 Value::String(_) => "str",
306 Value::DateTime(_) => "datetime",
307 Value::Seq(_) => "seq",
308 Value::Map(_) => "map",
309 }
310 }
311
312 pub fn to_json(&self) -> serde_json::Value {
315 use serde_json::Value as J;
316 match &self.value {
317 Value::Null => J::Null,
318 Value::Bool(b) => J::Bool(*b),
319 Value::Int(i) => J::from(*i),
320 Value::UInt(u) => J::from(*u),
321 Value::Float(f) => serde_json::Number::from_f64(*f).map_or(J::Null, J::Number),
322 Value::String(s) | Value::DateTime(s) => J::String(s.clone()),
323 Value::Seq(items) => J::Array(items.iter().map(Node::to_json).collect()),
324 Value::Map(entries) => J::Object(
325 entries
326 .iter()
327 .map(|(k, v)| (k.clone(), v.to_json()))
328 .collect(),
329 ),
330 }
331 }
332
333 pub fn walk<'a>(&'a self, mut visit: impl FnMut(&Path, &'a Node)) {
335 let mut stack: Vec<(Path, &'a Node)> = vec![(Path::root(), self)];
338 while let Some((path, node)) = stack.pop() {
339 visit(&path, node);
340 match &node.value {
341 Value::Seq(items) => {
342 for (i, item) in items.iter().enumerate().rev() {
343 stack.push((path.child_index(i), item));
344 }
345 }
346 Value::Map(entries) => {
347 for (key, value) in entries.iter().rev() {
348 stack.push((path.child_key(key), value));
349 }
350 }
351 _ => {}
352 }
353 }
354 }
355
356 pub fn redacted(&self, redactor: &dyn Redactor) -> Node {
359 redact::apply(self, redactor)
360 }
361}
362
363pub(crate) fn value_eq(a: &Node, b: &Node) -> bool {
366 match (&a.value, &b.value) {
367 (Value::Float(x), Value::Float(y)) => x.to_bits() == y.to_bits() || x == y,
368 (Value::Int(x), Value::UInt(y)) | (Value::UInt(y), Value::Int(x)) => {
369 u64::try_from(*x).is_ok_and(|x| x == *y)
370 }
371 (Value::Seq(x), Value::Seq(y)) => {
372 x.len() == y.len() && x.iter().zip(y).all(|(x, y)| value_eq(x, y))
373 }
374 (Value::Map(x), Value::Map(y)) => {
375 x.len() == y.len()
376 && x.iter()
377 .zip(y)
378 .all(|((kx, vx), (ky, vy))| kx == ky && value_eq(vx, vy))
379 }
380 (x, y) => x == y,
381 }
382}
383
384impl From<&serde_json::Value> for Node {
385 fn from(value: &serde_json::Value) -> Self {
386 use serde_json::Value as J;
387 Node::new(match value {
388 J::Null => Value::Null,
389 J::Bool(b) => Value::Bool(*b),
390 J::Number(n) => {
391 if let Some(i) = n.as_i64() {
392 Value::Int(i)
393 } else if let Some(u) = n.as_u64() {
394 Value::UInt(u)
395 } else {
396 Value::Float(n.as_f64().unwrap_or(f64::NAN))
397 }
398 }
399 J::String(s) => Value::String(s.clone()),
400 J::Array(items) => Value::Seq(items.iter().map(Node::from).collect()),
401 J::Object(entries) => Value::Map(
402 entries
403 .iter()
404 .map(|(k, v)| (k.clone(), Node::from(v)))
405 .collect(),
406 ),
407 })
408 }
409}
410
411impl From<serde_json::Value> for Node {
412 fn from(value: serde_json::Value) -> Self {
413 Node::from(&value)
414 }
415}
416
417pub fn from_serialize<T: serde::Serialize + ?Sized>(value: &T) -> Result<Node, DataError> {
432 value.serialize(ser::NodeSerializer)
433}
434
435pub(crate) fn scalar_text(value: &Value, quote: bool) -> (String, &'static str) {
443 match value {
444 Value::Null => ("null".into(), "json.null"),
445 Value::Bool(true) => ("true".into(), "json.bool_true"),
446 Value::Bool(false) => ("false".into(), "json.bool_false"),
447 Value::Int(i) => (i.to_string(), "json.number"),
448 Value::UInt(u) => (u.to_string(), "json.number"),
449 Value::Float(f) => (rich::pyformat::float_repr(*f), "json.number"),
450 Value::String(s) if quote => (quote_str(s), "json.str"),
451 Value::String(s) => (escape_controls(s), "json.str"),
452 Value::DateTime(s) => (s.clone(), "data.datetime"),
453 Value::Seq(_) | Value::Map(_) => (String::new(), "data.summary"),
454 }
455}
456
457pub fn display_value(node: &Node) -> String {
469 if node.is_container() {
470 summary(node)
471 } else {
472 scalar_text(&node.value, true).0
473 }
474}
475
476pub fn copy_text(node: &Node) -> String {
487 match &node.value {
488 Value::String(s) | Value::DateTime(s) => s.clone(),
489 _ if node.is_container() => {
490 serde_json::to_string_pretty(&node.to_json()).unwrap_or_default()
491 }
492 value => scalar_text(value, true).0,
493 }
494}
495
496pub(crate) fn quote_str(s: &str) -> String {
501 let quoted = serde_json::to_string(s).unwrap_or_else(|_| format!("{s:?}"));
502 if !quoted.chars().any(char::is_control) {
503 return quoted;
504 }
505 let mut out = String::with_capacity(quoted.len() + 8);
506 for c in quoted.chars() {
507 if c.is_control() {
508 out.push_str(&format!("\\u{:04x}", c as u32));
509 } else {
510 out.push(c);
511 }
512 }
513 out
514}
515
516pub(crate) fn escape_controls(s: &str) -> String {
519 if !s.chars().any(char::is_control) {
520 return s.to_string();
521 }
522 let quoted = quote_str(s);
523 quoted[1..quoted.len() - 1].replace("\\\"", "\"")
525}
526
527pub(crate) fn summary(node: &Node) -> String {
529 let plural = |n: usize, word: &str| {
530 if n == 1 {
531 format!("{n} {word}")
532 } else {
533 format!("{n} {word}s")
534 }
535 };
536 match &node.value {
537 Value::Map(e) if e.is_empty() => "{}".into(),
538 Value::Seq(i) if i.is_empty() => "[]".into(),
539 Value::Map(e) => format!("{{…}} {}", plural(e.len(), "key")),
540 Value::Seq(i) => format!("[…] {}", plural(i.len(), "item")),
541 _ => String::new(),
542 }
543}
544
545pub(crate) fn truncate_chars(text: &str, max: usize) -> Cow<'_, str> {
547 match text.char_indices().nth(max) {
548 Some((cut, _)) => Cow::Owned(format!("{}…", &text[..cut])),
549 None => Cow::Borrowed(text),
550 }
551}
552
553#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
559pub enum PathSegment {
560 Key(String),
561 Index(usize),
562}
563
564#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
568pub struct Path(Vec<PathSegment>);
569
570impl Path {
571 pub fn root() -> Self {
573 Path(Vec::new())
574 }
575
576 pub fn segments(&self) -> &[PathSegment] {
577 &self.0
578 }
579
580 pub fn is_root(&self) -> bool {
581 self.0.is_empty()
582 }
583
584 pub fn len(&self) -> usize {
585 self.0.len()
586 }
587
588 pub fn is_empty(&self) -> bool {
589 self.0.is_empty()
590 }
591
592 pub fn push(&mut self, segment: PathSegment) {
593 self.0.push(segment);
594 }
595
596 pub fn child_key(&self, key: &str) -> Path {
598 let mut path = self.clone();
599 path.0.push(PathSegment::Key(key.to_string()));
600 path
601 }
602
603 pub fn child_index(&self, index: usize) -> Path {
605 let mut path = self.clone();
606 path.0.push(PathSegment::Index(index));
607 path
608 }
609
610 pub fn parent(&self) -> Option<Path> {
612 (!self.0.is_empty()).then(|| Path(self.0[..self.0.len() - 1].to_vec()))
613 }
614
615 pub fn last(&self) -> Option<&PathSegment> {
616 self.0.last()
617 }
618
619 pub fn last_key(&self) -> Option<&str> {
622 self.0.iter().rev().find_map(|s| match s {
623 PathSegment::Key(k) => Some(k.as_str()),
624 PathSegment::Index(_) => None,
625 })
626 }
627}
628
629impl From<Vec<PathSegment>> for Path {
630 fn from(segments: Vec<PathSegment>) -> Self {
631 Path(segments)
632 }
633}
634
635pub(crate) fn is_identifier(key: &str) -> bool {
636 let mut chars = key.chars();
637 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
638 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
639}
640
641pub(crate) fn segment_text(segment: &PathSegment, first: bool) -> String {
643 match segment {
644 PathSegment::Index(i) => format!("[{i}]"),
645 PathSegment::Key(k) if is_identifier(k) && first => k.clone(),
646 PathSegment::Key(k) if is_identifier(k) => format!(".{k}"),
647 PathSegment::Key(k) => format!("[{}]", quote_str(k)),
648 }
649}
650
651impl fmt::Display for Path {
652 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653 for (i, segment) in self.0.iter().enumerate() {
654 f.write_str(&segment_text(segment, i == 0))?;
655 }
656 Ok(())
657 }
658}
659
660impl std::str::FromStr for Path {
661 type Err = String;
662
663 fn from_str(s: &str) -> Result<Self, Self::Err> {
666 let chars: Vec<char> = s.chars().collect();
667 let mut i = 0;
668 let mut path = Path::root();
669 if chars.first() == Some(&'$') {
670 i = 1;
671 }
672 let mut first = true;
673 while i < chars.len() {
674 match chars[i] {
675 '.' => {
676 i += 1;
677 let start = i;
678 while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
679 i += 1;
680 }
681 if start == i {
682 return Err(format!("empty key at column {}", start + 1));
683 }
684 path.push(PathSegment::Key(chars[start..i].iter().collect()));
685 }
686 '[' => {
687 i += 1;
688 if chars.get(i) == Some(&'"') {
689 let start = i;
690 i += 1;
691 while i < chars.len() && chars[i] != '"' {
692 if chars[i] == '\\' {
693 i += 1;
694 }
695 i += 1;
696 }
697 let literal: String =
698 chars[start..(i + 1).min(chars.len())].iter().collect();
699 let key: String = serde_json::from_str(&literal)
700 .map_err(|_| format!("bad quoted key at column {}", start + 1))?;
701 i += 1;
702 path.push(PathSegment::Key(key));
703 } else {
704 let start = i;
705 while i < chars.len() && chars[i].is_ascii_digit() {
706 i += 1;
707 }
708 let digits: String = chars[start..i].iter().collect();
709 let index = digits
710 .parse()
711 .map_err(|_| format!("expected an index at column {}", start + 1))?;
712 path.push(PathSegment::Index(index));
713 }
714 if chars.get(i) != Some(&']') {
715 return Err(format!("expected `]` at column {}", i + 1));
716 }
717 i += 1;
718 }
719 _ if first => {
720 let start = i;
721 while i < chars.len() && chars[i] != '.' && chars[i] != '[' {
722 i += 1;
723 }
724 path.push(PathSegment::Key(chars[start..i].iter().collect()));
725 }
726 c => return Err(format!("unexpected `{c}` at column {}", i + 1)),
727 }
728 first = false;
729 }
730 Ok(path)
731 }
732}
733
734#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
740pub enum Format {
741 Json,
742 Yaml,
743 Toml,
744 Xml,
745 Ini,
746 Dotenv,
747}
748
749impl Format {
750 pub const ALL: [Format; 6] = [
752 Format::Json,
753 Format::Yaml,
754 Format::Toml,
755 Format::Xml,
756 Format::Ini,
757 Format::Dotenv,
758 ];
759
760 pub fn name(self) -> &'static str {
762 match self {
763 Format::Json => "json",
764 Format::Yaml => "yaml",
765 Format::Toml => "toml",
766 Format::Xml => "xml",
767 Format::Ini => "ini",
768 Format::Dotenv => "dotenv",
769 }
770 }
771
772 pub fn from_name(name: &str) -> Option<Format> {
774 match name.to_ascii_lowercase().as_str() {
775 "json" => Some(Format::Json),
776 "yaml" | "yml" => Some(Format::Yaml),
777 "toml" => Some(Format::Toml),
778 "xml" => Some(Format::Xml),
779 "ini" => Some(Format::Ini),
780 "dotenv" | "env" => Some(Format::Dotenv),
781 _ => None,
782 }
783 }
784
785 pub fn from_extension(extension: &str) -> Option<Format> {
788 let extension = extension.strip_prefix('.').unwrap_or(extension);
789 match extension.to_ascii_lowercase().as_str() {
790 "json" => Some(Format::Json),
791 "yaml" | "yml" => Some(Format::Yaml),
792 "toml" => Some(Format::Toml),
793 "xml" => Some(Format::Xml),
794 "ini" | "cfg" => Some(Format::Ini),
795 "env" => Some(Format::Dotenv),
796 _ => None,
797 }
798 }
799
800 pub fn from_file_name(name: &str) -> Option<Format> {
802 let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
803 if base == ".env" || base.starts_with(".env.") {
804 return Some(Format::Dotenv);
805 }
806 let (stem, extension) = base.rsplit_once('.')?;
807 if stem.is_empty() {
808 return None;
809 }
810 Format::from_extension(extension)
811 }
812
813 pub fn is_enabled(self) -> bool {
815 match self {
816 Format::Json | Format::Ini | Format::Dotenv => true,
817 Format::Yaml => cfg!(feature = "yaml"),
818 Format::Toml => cfg!(feature = "toml"),
819 Format::Xml => cfg!(feature = "xml"),
820 }
821 }
822
823 pub fn detect(content: &str, name_hint: Option<&str>) -> Option<Format> {
862 if let Some(format) = name_hint.and_then(Format::from_file_name) {
863 if format.is_enabled() {
864 return Some(format);
865 }
866 }
867 let trimmed = content.trim_start_matches('\u{feff}').trim();
868 if trimmed.is_empty() {
869 return None;
870 }
871 if (trimmed.starts_with('{') || trimmed.starts_with('['))
872 && serde_json::from_str::<serde_json::Value>(trimmed)
873 .is_ok_and(|v| v.is_object() || v.is_array())
874 {
875 return Some(Format::Json);
876 }
877 #[cfg(feature = "xml")]
878 if xml::looks_like(trimmed) && xml::parse(content).is_ok() {
879 return Some(Format::Xml);
880 }
881 #[cfg(feature = "toml")]
882 if toml_doc::looks_like(content) {
883 return Some(Format::Toml);
884 }
885 if dotenv::looks_like(content) {
886 return Some(Format::Dotenv);
887 }
888 if ini::looks_like(content) {
889 return Some(Format::Ini);
890 }
891 #[cfg(feature = "yaml")]
892 if yaml::looks_like(content) {
893 return Some(Format::Yaml);
894 }
895 None
896 }
897}
898
899impl fmt::Display for Format {
900 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
901 f.write_str(match self {
902 Format::Json => "JSON",
903 Format::Yaml => "YAML",
904 Format::Toml => "TOML",
905 Format::Xml => "XML",
906 Format::Ini => "INI",
907 Format::Dotenv => "dotenv",
908 })
909 }
910}
911
912#[derive(Clone, Debug, PartialEq, Eq)]
914pub struct DataError {
915 pub format: Format,
916 pub message: String,
917 pub position: Option<Position>,
918}
919
920impl DataError {
921 pub fn new(format: Format, message: impl Into<String>, position: Option<Position>) -> Self {
925 DataError {
926 format,
927 message: escape_controls(&message.into()),
928 position,
929 }
930 }
931
932 pub fn to_diagnostic(&self, source: &str, name: &str) -> crate::diagnostic::Diagnostic {
949 use crate::diagnostic::{Diagnostic, Location, SourceSnippet};
950 use crate::event::EventView;
951 let mut diagnostic =
952 Diagnostic::error(format!("invalid {}: {}", self.format, self.message))
953 .view(EventView::Expanded);
954 let Some(position) = self.position else {
955 return diagnostic;
956 };
957 diagnostic = diagnostic.location(Location::new(
958 name,
959 Some(position.line),
960 Some(position.column),
961 ));
962 let start = byte_offset(source, position);
963 let end = source[start..]
964 .chars()
965 .next()
966 .filter(|c| *c != '\n' && *c != '\r')
967 .map_or(start, |c| start + c.len_utf8());
968 let (source, [start, end]) = picture_controls(source, [start, end]);
971 if let Ok(snippet) = SourceSnippet::new(name.to_string(), source, start..end, 1) {
972 diagnostic = diagnostic.snippet(snippet.primary_label(self.message.clone()));
973 }
974 diagnostic
975 }
976}
977
978fn picture_controls(source: &str, mut offsets: [usize; 2]) -> (String, [usize; 2]) {
983 let mut out = String::with_capacity(source.len());
984 let original = offsets;
985 let mut chars = source.char_indices().peekable();
986 while let Some((i, c)) = chars.next() {
987 for (offset, at) in offsets.iter_mut().zip(original) {
988 if at == i {
989 *offset = out.len();
990 }
991 }
992 let keep = matches!(c, '\n' | '\t')
993 || (c == '\r' && matches!(chars.peek(), None | Some((_, '\n'))));
994 out.push(match c {
995 _ if keep || !c.is_control() => c,
996 '\u{0}'..='\u{1f}' => char::from_u32(0x2400 + c as u32).unwrap_or('\u{fffd}'),
997 '\u{7f}' => '\u{2421}',
998 _ => '\u{fffd}',
999 });
1000 }
1001 for (offset, at) in offsets.iter_mut().zip(original) {
1002 if at >= source.len() {
1003 *offset = out.len();
1004 }
1005 }
1006 (out, offsets)
1007}
1008
1009impl fmt::Display for DataError {
1010 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1011 let message = escape_controls(&self.message);
1013 match self.position {
1014 Some(p) => write!(
1015 f,
1016 "invalid {} at line {}, column {}: {message}",
1017 self.format, p.line, p.column
1018 ),
1019 None => write!(f, "invalid {}: {message}", self.format),
1020 }
1021 }
1022}
1023
1024impl std::error::Error for DataError {}
1025
1026#[cfg(any(feature = "toml", feature = "xml"))]
1033pub(crate) struct LineIndex<'a> {
1034 source: &'a str,
1035 starts: Vec<usize>,
1036 last: std::cell::Cell<(usize, usize, usize)>,
1038}
1039
1040#[cfg(any(feature = "toml", feature = "xml"))]
1041impl<'a> LineIndex<'a> {
1042 pub(crate) fn new(source: &'a str) -> Self {
1043 let mut starts = vec![0];
1044 starts.extend(source.match_indices('\n').map(|(i, _)| i + 1));
1045 LineIndex {
1046 source,
1047 starts,
1048 last: std::cell::Cell::new((0, 1, 1)),
1049 }
1050 }
1051
1052 pub(crate) fn position(&self, offset: usize) -> Position {
1053 let mut offset = offset.min(self.source.len());
1054 while !self.source.is_char_boundary(offset) {
1055 offset -= 1;
1056 }
1057 let line = self.starts.partition_point(|s| *s <= offset);
1058 let start = self.starts[line - 1];
1059 let (last_offset, last_line, last_column) = self.last.get();
1060 let column = if last_line == line && last_offset >= start && last_offset <= offset {
1061 last_column + self.source[last_offset..offset].chars().count()
1062 } else {
1063 self.source[start..offset].chars().count() + 1
1064 };
1065 self.last.set((offset, line, column));
1066 Position::new(line, column)
1067 }
1068}
1069
1070pub(crate) fn byte_offset(source: &str, position: Position) -> usize {
1072 let mut offset = 0;
1073 for _ in 1..position.line {
1074 match source[offset..].find('\n') {
1075 Some(i) => offset += i + 1,
1076 None => return source.len(),
1077 }
1078 }
1079 let line_end = source[offset..]
1080 .find('\n')
1081 .map_or(source.len(), |i| offset + i);
1082 source[offset..line_end]
1083 .char_indices()
1084 .nth(position.column.saturating_sub(1))
1085 .map_or(line_end, |(i, _)| offset + i)
1086}
1087
1088pub fn parse(format: Format, content: &str) -> Result<Node, DataError> {
1097 match format {
1098 Format::Json => json::parse(content),
1099 Format::Ini => ini::parse(content),
1100 Format::Dotenv => dotenv::parse(content),
1101 #[cfg(feature = "yaml")]
1102 Format::Yaml => yaml::parse(content),
1103 #[cfg(feature = "toml")]
1104 Format::Toml => toml_doc::parse(content),
1105 #[cfg(feature = "xml")]
1106 Format::Xml => xml::parse(content),
1107 #[allow(unreachable_patterns)]
1108 other => Err(DataError::new(
1109 other,
1110 format!(
1111 "{} support is not compiled in (enable the `{}` feature)",
1112 other,
1113 other.name()
1114 ),
1115 None,
1116 )),
1117 }
1118}
1119
1120pub fn parse_json(content: &str) -> Result<Node, DataError> {
1124 json::parse(content)
1125}
1126
1127pub fn parse_ini(content: &str) -> Result<Node, DataError> {
1138 ini::parse(content)
1139}
1140
1141pub fn parse_dotenv(content: &str) -> Result<Node, DataError> {
1151 dotenv::parse(content)
1152}
1153
1154#[cfg(feature = "yaml")]
1164pub fn parse_yaml(content: &str) -> Result<Node, DataError> {
1165 yaml::parse(content)
1166}
1167
1168#[cfg(feature = "toml")]
1170pub fn parse_toml(content: &str) -> Result<Node, DataError> {
1171 toml_doc::parse(content)
1172}
1173
1174#[cfg(feature = "xml")]
1182pub fn parse_xml(content: &str) -> Result<Node, DataError> {
1183 xml::parse(content)
1184}
1185
1186#[cfg(all(test, any(feature = "toml", feature = "xml")))]
1187mod line_index_tests {
1188 use super::{LineIndex, Position};
1189
1190 fn naive(source: &str, offset: usize) -> Position {
1192 let mut offset = offset.min(source.len());
1193 while !source.is_char_boundary(offset) {
1194 offset -= 1;
1195 }
1196 let start = source[..offset].rfind('\n').map_or(0, |i| i + 1);
1197 let line = source[..offset].matches('\n').count() + 1;
1198 Position::new(line, source[start..offset].chars().count() + 1)
1199 }
1200
1201 #[test]
1202 fn cached_positions_match_counting_from_the_line_start() {
1203 let source = "ab\ncafé 👩👩👧 x\n\n<a b=\"é\">tëxt</a>\nlast";
1204 let index = LineIndex::new(source);
1205 let mut offsets: Vec<usize> = (0..=source.len() + 2).collect();
1208 let mut state = 7u64;
1209 for _ in 0..400 {
1210 state = state
1211 .wrapping_mul(6364136223846793005)
1212 .wrapping_add(1442695040888963407);
1213 offsets.push((state >> 33) as usize % (source.len() + 3));
1214 }
1215 for offset in offsets {
1216 assert_eq!(
1217 index.position(offset),
1218 naive(source, offset),
1219 "offset {offset}"
1220 );
1221 }
1222 }
1223}