Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
# The config file and the sections an embedder hands over as YAML. Safe Rust;
# deserialization only.
serde-saphyr = { version = "2.0", default-features = false, features = ["deserialize"] }
serde-saphyr = { version = "2.1", default-features = false, features = ["deserialize"] }
# The key path (`runtime.worker_threads`) a config error names; already in the
# tree through axum.
serde_path_to_error = "0.1"
Expand Down
39 changes: 38 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,45 @@ pub fn from_yaml<T: serde::de::DeserializeOwned>(yaml: &str) -> anyhow::Result<T
/// boolean where text is expected is an error, not the text it spells, so
/// `name: true` does not start a service called "true". Errors stay on one
/// line, without a snippet of the document, so a log record holds the whole
/// message.
/// message. One well-formed document without any node (empty, only comments,
/// a bare `---`) sets nothing: a type that refuses it is read from an empty
/// mapping. An explicit `null` is a node and keeps its meaning; two
/// documents or a broken directive stay errors.
pub(crate) fn parse_yaml<T: serde::de::DeserializeOwned>(yaml: &str) -> Result<T, YamlError> {
match parse_yaml_document(yaml) {
Err(refused) if is_one_empty_document(yaml) => {
parse_yaml_document("{}").map_err(|_| refused)
}
read => read,
}
}

/// Whether `yaml` is a single document the reader accepts and that holds no
/// node: the reader checks the syntax and the document count, the text that
/// nothing in it is an explicit `null`.
fn is_one_empty_document(yaml: &str) -> bool {
has_no_node(yaml)
&& matches!(
parse_yaml_document::<Option<serde::de::IgnoredAny>>(yaml),
Ok(None)
)
}

/// Whether `yaml` holds no node: blank and comment lines, document markers
/// (`---`, `...`) with at most a comment after them, and directives.
fn has_no_node(yaml: &str) -> bool {
yaml.lines().all(|line| {
let line = line.trim();
let rest = line
.strip_prefix("---")
.or_else(|| line.strip_prefix("..."))
.unwrap_or(line)
.trim_start();
rest.is_empty() || rest.starts_with('#') || line.starts_with('%')
})
}

fn parse_yaml_document<T: serde::de::DeserializeOwned>(yaml: &str) -> Result<T, YamlError> {
let mut path = None;
serde_saphyr::with_deserializer_from_str_with_options(
yaml,
Expand Down
40 changes: 40 additions & 0 deletions src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,46 @@ fn an_empty_document_is_the_default_config() {
}
}

#[test]
fn an_explicit_null_document_keeps_its_meaning() {
// `null` is a node, unlike an empty document: an optional reads it as
// absent and an untyped value as null, never as an empty mapping.
for yaml in ["null", "~", "--- null\n"] {
assert_eq!(
parse_yaml::<Option<String>>(yaml).unwrap(),
None,
"{yaml:?}"
);
assert_eq!(
parse_yaml::<serde_json::Value>(yaml).unwrap(),
serde_json::Value::Null,
"{yaml:?}"
);
}
// A config is a mapping; an explicit null is not one.
assert!(ProxyConfig::from_yaml_str("null").is_err());
}

#[test]
fn an_empty_document_keeps_an_optional_absent() {
// The empty-mapping reading applies only when the type refuses no node.
for yaml in ["", "# nothing\n", "---\n"] {
assert_eq!(
parse_yaml::<Option<String>>(yaml).unwrap(),
None,
"{yaml:?}"
);
}
}

#[test]
fn a_malformed_empty_looking_document_is_refused() {
// Two documents or a broken directive are errors, not an empty config.
for yaml in ["---\n---\n", "%YAML\n---\n"] {
assert!(ProxyConfig::from_yaml_str(yaml).is_err(), "{yaml:?}");
}
}

#[test]
fn a_repeated_key_is_refused() {
// Which of two values would win is not something to guess for a config.
Expand Down
Loading