Skip to content

g1t/services/security/src/yaml.rs

293 lines11,261 bytesCodeBlame
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
5use std::collections::HashMap;
6
7use yaml_rust2::parser::{Event, MarkedEventReceiver, Parser};
8use yaml_rust2::scanner::{Marker, TScalarStyle};
9
10/// A value and where it starts: 1-based line and column.
11#[derive(Clone, Debug, PartialEq)]
12pub struct Node {
13 pub value: Value,
14 pub line: u32,
15 pub column: u32,
16}
17
18#[derive(Clone, Debug, PartialEq)]
19pub 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
32impl 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)]
88pub 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.
96fn 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
118enum Open {
119 Seq(Vec<Node>, Marker, usize),
120 Map(Vec<(Node, Node)>, Option<Node>, Marker, usize),
121}
122
123#[derive(Default)]
124struct Builder {
125 stack: Vec<Open>,
126 anchors: HashMap<usize, Node>,
127 root: Option<Node>,
128 documents: u32,
129 error: Option<YamlError>,
130}
131
132fn at(mark: Marker) -> (u32, u32) {
133 (mark.line() as u32, mark.col() as u32 + 1)
134}
135
136impl 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
166impl 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.
198fn 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`.
226pub 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)]
243mod 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}