use super::parse::{SectionName, SectionOccurrence};
use super::{Config, ConfigError, Entry, ResolveLimits};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DecodedEntry {
pub name: Vec<u8>,
pub value: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DecodedSection {
pub name: Vec<u8>,
pub subsection: Option<Vec<u8>>,
pub entries: Vec<DecodedEntry>,
}
impl Config {
pub fn from_decoded_sections(
sections: Vec<DecodedSection>,
limits: ResolveLimits,
) -> Result<Self, ConfigError> {
let mut config = Self {
entries: Vec::new(),
sections: Vec::new(),
occurrences: Vec::new(),
};
let mut metadata_bytes = 0usize;
let mut expanded_bytes = 0usize;
for (ordinal, section) in sections.into_iter().enumerate() {
let error = |reason| ConfigError {
line: ordinal + 1,
reason,
};
if !valid_name(§ion.name) {
return Err(error("invalid decoded section name"));
}
let name = SectionName {
section: section.name,
subsection: section.subsection,
};
let header_bytes = name
.section
.len()
.checked_add(name.subsection.as_ref().map_or(0, Vec::len))
.ok_or_else(|| error("section metadata bytes"))?;
metadata_bytes = metadata_bytes
.checked_add(128)
.and_then(|n| n.checked_add(header_bytes))
.filter(|n| *n <= limits.bytes)
.ok_or_else(|| error("section metadata bytes"))?;
let start = config.entries.len();
for entry in section.entries {
if !valid_name(&entry.name)
|| !entry.name.first().is_some_and(u8::is_ascii_alphabetic)
{
return Err(error("invalid decoded variable name"));
}
if config.entries.len() >= limits.entries {
return Err(error("entry occurrences"));
}
metadata_bytes = metadata_bytes
.checked_add(8)
.filter(|n| *n <= limits.bytes)
.ok_or_else(|| error("section metadata bytes"))?;
expanded_bytes = expanded_bytes
.checked_add(header_bytes)
.and_then(|n| n.checked_add(entry.name.len()))
.and_then(|n| n.checked_add(entry.value.as_ref().map_or(0, Vec::len)))
.filter(|n| *n <= limits.bytes)
.ok_or_else(|| error("expanded bytes"))?;
config.entries.push(Entry {
line: 0,
origin: None,
section: name.section.clone(),
subsection: name.subsection.clone(),
name: entry.name,
value: entry.value,
});
}
config.occurrences.push(SectionOccurrence {
name: name.clone(),
start,
entries: (start..config.entries.len()).collect(),
});
config.sections.push(name);
}
Ok(config)
}
}
fn valid_name(name: &[u8]) -> bool {
!name.is_empty() && name.iter().all(|b| b.is_ascii_alphanumeric() || *b == b'-')
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn section(name: &[u8], entries: Vec<DecodedEntry>) -> DecodedSection {
DecodedSection {
name: name.to_vec(),
subsection: None,
entries,
}
}
fn entry(name: &[u8], value: Option<&[u8]>) -> DecodedEntry {
DecodedEntry {
name: name.to_vec(),
value: value.map(Vec::from),
}
}
#[test]
fn preserves_decoded_bytes_membership_and_implicit_values() {
let mut first = section(
b"ReMoTe",
vec![
entry(b"URL", None),
entry(b"url", Some(b"")),
entry(b"url", Some(b" \xff\0\n ")),
],
);
first.subsection = Some(b"Old\0\xff".to_vec());
let config = Config::from_decoded_sections(
vec![
first,
section(b"remote", vec![]),
section(b"remote", vec![entry(b"url", Some(b"last"))]),
],
ResolveLimits::default(),
)
.unwrap();
assert_eq!(
config
.values("remote", Some(b"Old\0\xff"), "url")
.collect::<Vec<_>>(),
vec![None, Some(b"".as_slice()), Some(b" \xff\0\n ".as_slice())]
);
let sections: Vec<_> = config.section_occurrences().collect();
assert_eq!(sections.len(), 3);
assert_eq!(sections[0].name(), b"ReMoTe");
assert_eq!(sections[0].entry_indices(), &[0, 1, 2]);
assert!(sections[1].entry_indices().is_empty());
assert_eq!(sections[2].entry_indices(), &[3]);
assert!(
config
.entries()
.iter()
.all(|e| e.origin.is_none() && e.line == 0)
);
}
#[rstest]
#[case(b"", b"x")]
#[case(b"a.b", b"x")]
#[case(b"a\0", b"x")]
#[case(b"a", b"")]
#[case(b"a", b"1x")]
#[case(b"a", b"x_y")]
fn rejects_invalid_names(#[case] name: &[u8], #[case] key: &[u8]) {
let result = Config::from_decoded_sections(
vec![
section(b"valid", vec![]),
section(name, vec![entry(key, None)]),
],
ResolveLimits::default(),
);
assert_eq!(result.unwrap_err().line, 2);
}
#[rstest]
#[case::metadata_exact(137, 1, 0, true)]
#[case::metadata_short(136, 1, 0, false)]
#[case::entries_short(137, 0, 0, false)]
#[case::expanded_exact(137, 1, 135, true)]
#[case::expanded_short(137, 1, 136, false)]
fn enforces_independent_budgets(
#[case] bytes: usize,
#[case] entries: usize,
#[case] value_len: usize,
#[case] accepted: bool,
) {
let limits = ResolveLimits {
bytes,
entries,
depth: 0,
..ResolveLimits::default()
};
let result = Config::from_decoded_sections(
vec![section(
b"a",
vec![entry(b"b", Some(&vec![b'x'; value_len]))],
)],
limits,
);
assert_eq!(result.is_ok(), accepted);
}
#[test]
fn empty_headers_consume_metadata_without_entries() {
let limits = ResolveLimits {
bytes: 257,
entries: 0,
depth: 0,
..ResolveLimits::default()
};
let error = Config::from_decoded_sections(
vec![section(b"a", vec![]), section(b"a", vec![])],
limits,
)
.unwrap_err();
assert_eq!(error.line, 2);
assert_eq!(error.reason, "section metadata bytes");
}
#[test]
fn decoded_include_remains_inert() {
let config = Config::from_decoded_sections(
vec![section(
b"include",
vec![entry(b"path", Some(b"/missing/should-not-be-read"))],
)],
ResolveLimits::default(),
)
.unwrap();
assert_eq!(config.entries().len(), 1);
assert_eq!(
config.value("include", None, "path"),
Some(Some(b"/missing/should-not-be-read".as_slice()))
);
}
#[test]
fn preserves_empty_subsection_and_accepts_empty_input_at_zero_budget() {
let empty = Config::from_decoded_sections(
vec![],
ResolveLimits {
bytes: 0,
entries: 0,
..ResolveLimits::default()
},
)
.unwrap();
assert_eq!(empty.section_occurrences().len(), 0);
let config = Config::from_decoded_sections(
vec![
DecodedSection {
name: b"remote".to_vec(),
subsection: Some(vec![]),
entries: vec![],
},
section(b"remote", vec![]),
],
ResolveLimits::default(),
)
.unwrap();
assert_eq!(config.subsection_names("remote"), vec![b"".as_slice()]);
assert_eq!(
config.section_occurrences().next().unwrap().subsection(),
Some(b"".as_slice())
);
}
}