| 1 | //! YAML read with where each value is, so that a problem in the dependency |
| 2 | //! update file can name its line. Only what a configuration file needs: |
| 3 | //! mappings, sequences and scalars, with anchors and aliases resolved. |
| 4 | |
| 5 | use std::collections::HashMap; |
| 6 | |
| 7 | use yaml_rust2::parser::{Event, MarkedEventReceiver, Parser}; |
| 8 | use yaml_rust2::scanner::{Marker, TScalarStyle}; |
| 9 | |
| 10 | /// A value and where it starts: 1-based line and column. |
| 11 | #[derive(Clone, Debug, PartialEq)] |
| 12 | pub struct Node { |
| 13 | pub value: Value, |
| 14 | pub line: u32, |
| 15 | pub column: u32, |
| 16 | } |
| 17 | |
| 18 | #[derive(Clone, Debug, PartialEq)] |
| 19 | pub enum Value { |
| 20 | Null, |
| 21 | Bool(bool), |
| 22 | Int(i64), |
| 23 | Float(f64), |
| 24 | /// Text. `quoted` when it was written in quotes or as a block, which |
| 25 | /// keeps `"2"` text where a plain `2` is a number. |
| 26 | Text { text: String, quoted: bool }, |
| 27 | Seq(Vec<Node>), |
| 28 | /// In the order written. Keys are text; a duplicate is an error. |
| 29 | Map(Vec<(Node, Node)>), |
| 30 | } |
| 31 | |
| 32 | impl Node { |
| 33 | pub fn kind(&self) -> &'static str { |
| 34 | match self.value { |
| 35 | Value::Null => "empty", |
| 36 | Value::Bool(_) => "true or false", |
| 37 | Value::Int(_) | Value::Float(_) => "a number", |
| 38 | Value::Text { .. } => "text", |
| 39 | Value::Seq(_) => "a list", |
| 40 | Value::Map(_) => "a mapping", |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | /// The text of a scalar, however it was written: `2`, `true`, `"x"`. |
| 45 | pub fn scalar(&self) -> Option<String> { |
| 46 | Some(match &self.value { |
| 47 | Value::Text { text, .. } => text.clone(), |
| 48 | Value::Int(n) => n.to_string(), |
| 49 | Value::Float(n) => n.to_string(), |
| 50 | Value::Bool(b) => b.to_string(), |
| 51 | _ => return None, |
| 52 | }) |
| 53 | } |
| 54 | |
| 55 | pub fn as_map(&self) -> Option<&[(Node, Node)]> { |
| 56 | match &self.value { |
| 57 | Value::Map(entries) => Some(entries), |
| 58 | _ => None, |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | pub fn get(&self, key: &str) -> Option<&Node> { |
| 63 | self.as_map()? |
| 64 | .iter() |
| 65 | .find(|(name, _)| matches!(&name.value, Value::Text { text, .. } if text == key)) |
| 66 | .map(|(_, value)| value) |
| 67 | } |
| 68 | |
| 69 | /// As JSON, for showing what was read. |
| 70 | pub fn to_json(&self) -> serde_json::Value { |
| 71 | use serde_json::Value as Json; |
| 72 | match &self.value { |
| 73 | Value::Null => Json::Null, |
| 74 | Value::Bool(b) => Json::Bool(*b), |
| 75 | Value::Int(n) => Json::from(*n), |
| 76 | Value::Float(n) => serde_json::Number::from_f64(*n).map_or(Json::Null, Json::Number), |
| 77 | Value::Text { text, .. } => Json::String(text.clone()), |
| 78 | Value::Seq(items) => Json::Array(items.iter().map(Node::to_json).collect()), |
| 79 | Value::Map(entries) => Json::Object( |
| 80 | entries.iter().map(|(key, value)| (key.scalar().unwrap_or_default(), value.to_json())).collect(), |
| 81 | ), |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | /// What went wrong reading the YAML itself. |
| 87 | #[derive(Debug, PartialEq)] |
| 88 | pub struct YamlError { |
| 89 | pub line: u32, |
| 90 | pub column: u32, |
| 91 | pub message: String, |
| 92 | } |
| 93 | |
| 94 | /// YAML 1.2's core schema, as Dependabot reads it: `true`, `false`, |
| 95 | /// `null`, `~`, integers and floats are themselves in a plain scalar. |
| 96 | fn plain(text: String) -> Value { |
| 97 | match text.as_str() { |
| 98 | "" | "~" | "null" | "Null" | "NULL" => return Value::Null, |
| 99 | "true" | "True" | "TRUE" => return Value::Bool(true), |
| 100 | "false" | "False" | "FALSE" => return Value::Bool(false), |
| 101 | _ => {} |
| 102 | } |
| 103 | let digits = text.strip_prefix(['-', '+']).unwrap_or(&text); |
| 104 | if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) |
| 105 | && let Ok(n) = text.parse::<i64>() |
| 106 | { |
| 107 | return Value::Int(n); |
| 108 | } |
| 109 | if text.chars().any(|c| c.is_ascii_digit()) |
| 110 | && text.chars().all(|c| c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | 'e' | 'E')) |
| 111 | && let Ok(n) = text.parse::<f64>() |
| 112 | { |
| 113 | return Value::Float(n); |
| 114 | } |
| 115 | Value::Text { text, quoted: false } |
| 116 | } |
| 117 | |
| 118 | enum Open { |
| 119 | Seq(Vec<Node>, Marker, usize), |
| 120 | Map(Vec<(Node, Node)>, Option<Node>, Marker, usize), |
| 121 | } |
| 122 | |
| 123 | #[derive(Default)] |
| 124 | struct Builder { |
| 125 | stack: Vec<Open>, |
| 126 | anchors: HashMap<usize, Node>, |
| 127 | root: Option<Node>, |
| 128 | documents: u32, |
| 129 | error: Option<YamlError>, |
| 130 | } |
| 131 | |
| 132 | fn at(mark: Marker) -> (u32, u32) { |
| 133 | (mark.line() as u32, mark.col() as u32 + 1) |
| 134 | } |
| 135 | |
| 136 | impl Builder { |
| 137 | fn push(&mut self, node: Node, anchor: usize) { |
| 138 | if anchor > 0 { |
| 139 | self.anchors.insert(anchor, node.clone()); |
| 140 | } |
| 141 | match self.stack.last_mut() { |
| 142 | Some(Open::Seq(items, _, _)) => items.push(node), |
| 143 | Some(Open::Map(entries, key, _, _)) => match key.take() { |
| 144 | None => { |
| 145 | let duplicate = entries.iter().any(|(existing, _)| existing.scalar().is_some() && existing.scalar() == node.scalar()); |
| 146 | if duplicate && self.error.is_none() { |
| 147 | self.error = Some(YamlError { |
| 148 | line: node.line, |
| 149 | column: node.column, |
| 150 | message: format!("`{}` is given twice.", node.scalar().unwrap_or_default()), |
| 151 | }); |
| 152 | } |
| 153 | *key = Some(node); |
| 154 | } |
| 155 | Some(name) => entries.push((name, node)), |
| 156 | }, |
| 157 | None => { |
| 158 | if self.root.is_none() { |
| 159 | self.root = Some(node); |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | impl MarkedEventReceiver for Builder { |
| 167 | fn on_event(&mut self, event: Event, mark: Marker) { |
| 168 | let (line, column) = at(mark); |
| 169 | match event { |
| 170 | Event::DocumentStart => self.documents += 1, |
| 171 | Event::Scalar(text, style, anchor, _) => { |
| 172 | let value = if style == TScalarStyle::Plain { plain(text) } else { Value::Text { text, quoted: true } }; |
| 173 | self.push(Node { value, line, column }, anchor); |
| 174 | } |
| 175 | Event::Alias(anchor) => { |
| 176 | let node = self.anchors.get(&anchor).cloned().unwrap_or(Node { value: Value::Null, line, column }); |
| 177 | self.push(Node { line, column, ..node }, 0); |
| 178 | } |
| 179 | Event::SequenceStart(anchor, _) => self.stack.push(Open::Seq(Vec::new(), mark, anchor)), |
| 180 | Event::MappingStart(anchor, _) => self.stack.push(Open::Map(Vec::new(), None, mark, anchor)), |
| 181 | Event::SequenceEnd | Event::MappingEnd => { |
| 182 | let (value, mark, anchor) = match self.stack.pop() { |
| 183 | Some(Open::Seq(items, mark, anchor)) => (Value::Seq(items), mark, anchor), |
| 184 | Some(Open::Map(entries, _, mark, anchor)) => (Value::Map(merged(entries)), mark, anchor), |
| 185 | None => return, |
| 186 | }; |
| 187 | let (line, column) = at(mark); |
| 188 | self.push(Node { value, line, column }, anchor); |
| 189 | } |
| 190 | _ => {} |
| 191 | } |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | /// YAML's merge key, `<<: *defaults`, which configuration files use to |
| 196 | /// share settings between entries: the merged mapping's keys come first |
| 197 | /// unless the mapping sets them itself. |
| 198 | fn merged(entries: Vec<(Node, Node)>) -> Vec<(Node, Node)> { |
| 199 | if !entries.iter().any(|(key, _)| key.scalar().as_deref() == Some("<<")) { |
| 200 | return entries; |
| 201 | } |
| 202 | let mut own = Vec::new(); |
| 203 | let mut inherited = Vec::new(); |
| 204 | for (key, value) in entries { |
| 205 | if key.scalar().as_deref() != Some("<<") { |
| 206 | own.push((key, value)); |
| 207 | continue; |
| 208 | } |
| 209 | let sources = match value.value { |
| 210 | Value::Seq(items) => items, |
| 211 | _ => vec![value], |
| 212 | }; |
| 213 | for source in sources { |
| 214 | if let Value::Map(found) = source.value { |
| 215 | inherited.extend(found); |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | inherited.retain(|(key, _)| !own.iter().any(|(mine, _)| mine.scalar() == key.scalar())); |
| 220 | inherited.extend(own); |
| 221 | inherited |
| 222 | } |
| 223 | |
| 224 | /// The single document in `source`, or where it stops being YAML. An empty |
| 225 | /// file is `Null`. |
| 226 | pub fn parse(source: &str) -> Result<Node, YamlError> { |
| 227 | let mut builder = Builder::default(); |
| 228 | let mut parser = Parser::new_from_str(source); |
| 229 | if let Err(error) = parser.load(&mut builder, true) { |
| 230 | let (line, column) = at(*error.marker()); |
| 231 | return Err(YamlError { line, column, message: format!("This is not valid YAML: {}.", error.info()) }); |
| 232 | } |
| 233 | if let Some(error) = builder.error { |
| 234 | return Err(error); |
| 235 | } |
| 236 | if builder.documents > 1 { |
| 237 | return Err(YamlError { line: 0, column: 0, message: "The file holds more than one YAML document; it must hold one.".to_owned() }); |
| 238 | } |
| 239 | Ok(builder.root.unwrap_or(Node { value: Value::Null, line: 1, column: 1 })) |
| 240 | } |
| 241 | |
| 242 | #[cfg(test)] |
| 243 | mod tests { |
| 244 | use super::*; |
| 245 | |
| 246 | #[test] |
| 247 | fn values_keep_their_lines() { |
| 248 | let root = parse("version: 2\nupdates:\n - package-ecosystem: \"npm\"\n directory: /\n labels: [a, b]\n").unwrap(); |
| 249 | assert_eq!(root.get("version").unwrap().value, Value::Int(2)); |
| 250 | let entry = &match &root.get("updates").unwrap().value { |
| 251 | Value::Seq(items) => items.clone(), |
| 252 | _ => panic!(), |
| 253 | }[0]; |
| 254 | let ecosystem = entry.get("package-ecosystem").unwrap(); |
| 255 | assert_eq!((ecosystem.line, ecosystem.column), (3, 24)); |
| 256 | assert_eq!(ecosystem.value, Value::Text { text: "npm".into(), quoted: true }); |
| 257 | assert_eq!(entry.get("directory").unwrap().line, 4); |
| 258 | assert_eq!(entry.get("labels").unwrap().to_json(), serde_json::json!(["a", "b"])); |
| 259 | } |
| 260 | |
| 261 | #[test] |
| 262 | fn scalars_are_typed_as_yaml_does() { |
| 263 | let root = parse("a: 2\nb: \"2\"\nc: true\nd: ~\ne: 1.5\nf: 03:00\ng: v1\n").unwrap(); |
| 264 | assert_eq!(root.get("a").unwrap().value, Value::Int(2)); |
| 265 | assert_eq!(root.get("b").unwrap().value, Value::Text { text: "2".into(), quoted: true }); |
| 266 | assert_eq!(root.get("c").unwrap().value, Value::Bool(true)); |
| 267 | assert_eq!(root.get("d").unwrap().value, Value::Null); |
| 268 | assert_eq!(root.get("e").unwrap().value, Value::Float(1.5)); |
| 269 | assert_eq!(root.get("f").unwrap().scalar().as_deref(), Some("03:00")); |
| 270 | assert_eq!(root.get("g").unwrap().scalar().as_deref(), Some("v1")); |
| 271 | } |
| 272 | |
| 273 | #[test] |
| 274 | fn anchors_and_merge_keys_are_resolved() { |
| 275 | let source = "defaults: &d\n interval: weekly\n day: monday\nschedule:\n <<: *d\n day: friday\nalias: *d\n"; |
| 276 | let root = parse(source).unwrap(); |
| 277 | let schedule = root.get("schedule").unwrap(); |
| 278 | assert_eq!(schedule.get("interval").unwrap().scalar().as_deref(), Some("weekly")); |
| 279 | assert_eq!(schedule.get("day").unwrap().scalar().as_deref(), Some("friday")); |
| 280 | assert_eq!(root.get("alias").unwrap().get("day").unwrap().scalar().as_deref(), Some("monday")); |
| 281 | } |
| 282 | |
| 283 | #[test] |
| 284 | fn broken_yaml_and_duplicates_say_where() { |
| 285 | let error = parse("version: 2\nupdates: [\n").unwrap_err(); |
| 286 | assert!(error.message.starts_with("This is not valid YAML"), "{error:?}"); |
| 287 | assert!(error.line >= 2); |
| 288 | let twice = parse("version: 2\nversion: 3\n").unwrap_err(); |
| 289 | assert_eq!((twice.line, twice.message.as_str()), (2, "`version` is given twice.")); |
| 290 | assert_eq!(parse("").unwrap().value, Value::Null); |
| 291 | assert!(parse("a: 1\n---\nb: 2\n").is_err()); |
| 292 | } |
| 293 | } |