openmoat_core/yaml.rs
1//! The one place OpenMoat reads and writes YAML (ADR-024).
2//!
3//! Every policy, repository policy, overlay and fixture goes through these two
4//! functions, so the parser behind them can change without touching a caller.
5//! The parser is `serde-saphyr`, a pure-Rust YAML 1.2 parser, set up to stay
6//! as strict as the policy needs:
7//!
8//! - only `true` and `false` are booleans: YAML 1.1's `yes`, `no`, `on` and
9//! `off` are not read as booleans, so a value never changes type unseen;
10//! - a key that appears twice in one mapping is an error, not "last one wins";
11//! - merge keys (`<<`) are an error rather than a silent copy of another rule;
12//! - a tag the parser does not know (`!env TOKEN`) is an error, and `!!binary`
13//! is not decoded: the text a reviewer sees is the text the policy holds.
14//!
15//! Anchors and aliases (`&secret-paths`, `*secret-paths`) work as before, with
16//! the parser's limits on alias expansion.
17
18use std::fmt;
19
20use serde::Serialize;
21use serde::de::DeserializeOwned;
22use serde_saphyr::options::MergeKeyPolicy;
23
24/// YAML that does not parse, does not fit the target type, or cannot be written.
25/// Its message names the line and column where the parser stopped.
26#[derive(Debug)]
27pub struct Error(Inner);
28
29#[derive(Debug)]
30enum Inner {
31 Read(serde_saphyr::Error),
32 Write(serde_saphyr::ser::Error),
33}
34
35impl fmt::Display for Error {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match &self.0 {
38 Inner::Read(e) => e.fmt(f),
39 Inner::Write(e) => e.fmt(f),
40 }
41 }
42}
43
44impl std::error::Error for Error {}
45
46/// Parse one YAML document into `T`.
47///
48/// # Errors
49/// The text is not one well-formed YAML document, repeats a key, uses a merge
50/// key, an unknown tag or a YAML 1.1 boolean, or does not fit `T`.
51pub fn from_str<T: DeserializeOwned>(yaml: &str) -> Result<T, Error> {
52 let options = serde_saphyr::options! {
53 strict_booleans: true,
54 merge_keys: MergeKeyPolicy::Error,
55 reject_unsupported_tags: true,
56 ignore_binary_tag_for_string: true,
57 // One line per error: it ends up in a hook's deny reason.
58 with_snippet: false,
59 };
60 serde_saphyr::from_str_with_options(yaml, options).map_err(|e| Error(Inner::Read(e)))
61}
62
63/// Write `value` as a YAML document.
64///
65/// # Errors
66/// `value` has a shape YAML cannot hold (a map with non-scalar keys, say).
67pub fn to_string<T: Serialize>(value: &T) -> Result<String, Error> {
68 serde_saphyr::to_string(value).map_err(|e| Error(Inner::Write(e)))
69}
70
71#[cfg(test)]
72mod tests;