use std::fmt::Write as _;
use std::{borrow::Cow, collections::BTreeMap, iter::Peekable, str::Chars};
pub const KEY_REFUSEMANUALSTART: &str = "RefuseManualStart";
pub const KEY_REFUSEMANUALSTOP: &str = "RefuseManualStop";
pub const KEY_X_RELOADIFCHANGED: &str = "X-ReloadIfChanged";
pub const KEY_X_RESTARTIFCHANGED: &str = "X-RestartIfChanged";
pub const KEY_X_STOPIFCHANGED: &str = "X-StopIfChanged";
pub const KEY_X_SWITCHMETHOD: &str = "X-SwitchMethod";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemdIni(Sections);
type Sections = BTreeMap<String, Entries>;
type Entries = BTreeMap<String, Value>;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Value {
Strings(Vec<String>),
SwitchMethod(UnitSwitchMethod),
Bool(bool),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum UnitSwitchMethod {
Reload,
Restart,
StopStart,
StopOnly,
KeepOld,
}
impl SystemdIni {
pub fn get_bool(&self, section: &str, key: &str) -> Option<bool> {
self.get_value(section, key).and_then(|v| {
if let Value::Bool(b) = v {
Some(*b)
} else {
None
}
})
}
pub fn get_unit_switch_method(&self) -> Option<UnitSwitchMethod> {
self.get_value("Unit", KEY_X_SWITCHMETHOD).and_then(|v| {
if let Value::SwitchMethod(b) = v {
Some(*b)
} else {
None
}
})
}
fn get_value(&self, section: &str, key: &str) -> Option<&Value> {
self.0.get(section).and_then(|entries| entries.get(key))
}
pub fn remove(&mut self, section: &str, key: &str) {
let _ = self.0.get_mut(section).map(|entries| entries.remove(key));
}
pub fn extend(self: &mut SystemdIni, other: SystemdIni) {
for (section, entries) in other.0 {
self.0.entry(section).or_default().extend(entries);
}
}
pub fn eq_excluding(&self, other: &SystemdIni, excluded: &[(&str, &str)]) -> bool {
fn make_eq_iter<'a>(
ini: &'a SystemdIni,
excluded: &'a [(&'a str, &'a str)],
) -> impl Iterator<Item = ((&'a str, &'a str), &'a Value)> {
ini.0
.iter()
.flat_map(|(section, entries)| {
entries
.iter()
.map(|(key, value)| ((section.as_str(), key.as_str()), value))
})
.filter(|(p, _)| !excluded.contains(p))
}
let a = make_eq_iter(self, excluded);
let b = make_eq_iter(other, excluded);
a.eq(b)
}
}
struct Parser<'a> {
content: Peekable<Chars<'a>>,
line: usize,
column: usize,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ParseError {
message: Cow<'static, str>,
line: usize,
column: usize,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} at line {}, column {}",
self.message, self.line, self.column
)
}
}
impl std::error::Error for ParseError {}
impl<'a> Parser<'a> {
fn eof(&mut self) -> bool {
self.content.peek().is_none()
}
fn next(&mut self) -> Option<char> {
let ch = self.content.next()?;
if ch == '\n' {
self.line += 1;
self.column = 0;
} else {
self.column += 1;
}
Some(ch)
}
fn next_if<P>(&mut self, p: P) -> Option<char>
where
P: FnOnce(&char) -> bool,
{
let ch = self.content.next_if(p)?;
if ch == '\n' {
self.line += 1;
self.column = 0;
} else {
self.column += 1;
}
Some(ch)
}
fn error(&self, message: Cow<'static, str>) -> ParseError {
ParseError {
message,
line: self.line,
column: self.column,
}
}
fn error_unexpected<S: AsRef<str>>(&mut self, expected: S) -> ParseError {
let message = format!(
"expected {} but got {}",
expected.as_ref(),
if let Some(ch) = self.content.peek() {
format!("'{ch}'")
} else {
"end of file".to_string()
}
);
ParseError {
message: message.into(),
line: self.line,
column: self.column,
}
}
fn expect(&mut self, expected: char) -> Result<(), ParseError> {
if let Some(ch) = self.content.peek() {
let ch = *ch;
if ch == expected {
self.next();
return Ok(());
}
}
Err(self.error_unexpected(format!("'{expected}'")))
}
#[allow(dead_code)]
fn take(&mut self, n: usize) -> String {
let mut result = String::new();
for _ in 0..n {
if let Some(ch) = self.next() {
result.push(ch);
} else {
break;
}
}
result
}
fn take_while<P>(&mut self, p: P) -> String
where
P: Fn(&char) -> bool,
{
let mut result = String::new();
while let Some(ch) = self.next_if(&p) {
result.push(ch);
}
result
}
fn skip_while<P>(&mut self, p: P)
where
P: Fn(&char) -> bool,
{
while self.next_if(&p).is_some() {}
}
fn skip_ws(&mut self) {
self.skip_while(|ch| ch.is_ascii_whitespace())
}
fn skip_hs(&mut self) {
self.skip_while(|ch| *ch == ' ' || *ch == '\t')
}
fn skip_comment(&mut self) -> bool {
if self.next_if(|ch| *ch == '#' || *ch == ';').is_some() {
self.skip_while(|ch| *ch != '\n');
self.next();
true
} else {
false
}
}
fn parse_section(&mut self) -> Result<Option<(String, Entries)>, ParseError> {
self.skip_ws();
while self.skip_comment() {
self.skip_ws();
}
if self.eof() {
return Ok(None);
}
self.expect('[')?;
let section_name = self
.take_while(|ch| *ch != ']' && *ch != '\'' && *ch != '\"' && !ch.is_ascii_control());
self.expect(']')?;
self.expect('\n')?;
self.skip_ws();
let entries = self.parse_section_entries(§ion_name)?;
Ok(Some((section_name, entries)))
}
fn parse_section_entries(&mut self, section_name: &str) -> Result<Entries, ParseError> {
let mut entries = Entries::new();
while let Some(ch) = self.content.peek() {
if *ch == '[' {
break;
}
if self.skip_comment() {
self.skip_ws();
continue;
}
let key = self.parse_entry_key()?;
if key.is_empty() {
return Err(self.error_unexpected("entry key name"));
}
self.skip_hs();
self.expect('=')?;
self.skip_hs();
let value = self.parse_entry_value()?;
match section_name {
"Unit" => {
if [
KEY_REFUSEMANUALSTART,
KEY_REFUSEMANUALSTOP,
KEY_X_RELOADIFCHANGED,
KEY_X_RESTARTIFCHANGED,
KEY_X_STOPIFCHANGED,
]
.contains(&key.as_str())
{
let value = to_bool(value).map_err(|s| self.error(s.into()))?;
entries.insert(key, value);
} else if [KEY_X_SWITCHMETHOD].contains(&key.as_str()) {
let value =
to_unit_switch_method(&value).map_err(|s| self.error(s.into()))?;
entries.insert(key, value);
} else {
let e = entries.entry(key).or_insert_with(|| Value::Strings(vec![]));
match e {
Value::Strings(items) => items.push(value),
_ => panic!("inconsistent key value type"),
};
}
}
_ => {
let e = entries.entry(key).or_insert_with(|| Value::Strings(vec![]));
match e {
Value::Strings(items) => items.push(value),
_ => panic!("inconsistent key value type"),
};
}
};
self.skip_ws();
}
Ok(entries)
}
fn parse_entry_key(&mut self) -> Result<String, ParseError> {
Ok(self.take_while(|c| c.is_ascii_alphanumeric() || *c == '-'))
}
fn parse_entry_value(&mut self) -> Result<String, ParseError> {
let mut value = String::with_capacity(16);
while let Some(ch) = self.next() {
if ch == '\n' {
break;
} else if ch == '\\' {
let Some(ch) = self.next() else {
return Err(self.error("invalid character escape: '\\'".into()));
};
match ch {
'\n' => {
value.push(' ');
self.skip_hs();
while self.skip_comment() {
self.skip_hs();
}
}
ch => {
value.push('\\');
value.push(ch);
}
}
} else {
value.push(ch);
}
}
Ok(value.trim().to_string())
}
}
fn to_bool(mut value: String) -> Result<Value, String> {
value.make_ascii_lowercase();
match value.as_str() {
"1" | "yes" | "y" | "true" | "t" | "on" => Ok(Value::Bool(true)),
"0" | "no" | "n" | "false" | "f" | "off" => Ok(Value::Bool(false)),
_ => Err(format!("invalid Boolean value \"{value}\"")),
}
}
fn to_unit_switch_method(value: &str) -> Result<Value, String> {
let value = match value {
"reload" => Ok(UnitSwitchMethod::Reload),
"restart" => Ok(UnitSwitchMethod::Restart),
"stop-start" => Ok(UnitSwitchMethod::StopStart),
"keep-old" => Ok(UnitSwitchMethod::KeepOld),
unknown => Err(format!("unknown unit switch method \"{unknown}\"")),
}?;
Ok(Value::SwitchMethod(value))
}
#[allow(dead_code)]
fn to_unescaped_string(value: &str) -> Result<String, String> {
let mut result = String::with_capacity(value.len());
let mut cur_escape: Vec<u8> = Vec::with_capacity(4);
let mut it = value.chars();
while let Some(ch) = it.next() {
if ch == '\\' {
let Some(ch) = it.next() else {
return Err("invalid character escape: '\\'".into());
};
match ch {
'a' => result.push('\x07'),
'b' => result.push('\x08'),
'f' => result.push('\x0C'),
'n' => result.push('\n'),
'r' => result.push('\r'),
't' => result.push('\t'),
'v' => result.push('\x0B'),
'\\' => result.push('\\'),
'"' => result.push('\"'),
'\'' => result.push('\''),
's' => result.push(' '),
'x' => {
let hex: String = it.by_ref().take(2).collect();
match (hex.len(), u8::from_str_radix(&hex, 16)) {
(2, Ok(char_num)) => {
cur_escape.push(char_num);
match std::str::from_utf8(&cur_escape) {
Ok(s) => {
result.push_str(s);
cur_escape.clear();
}
Err(e) if e.error_len().is_none() => {}
Err(_) => {
return Err(format!(
"invalid escaped UTF-8 code point '{}'",
to_hex_string(&cur_escape)
))
}
}
}
_ => return Err(format!("invalid character escape: '\\x{hex}'")),
}
}
n if ('0'..='7').contains(&n) => {
let mut oct = String::with_capacity(3);
oct.push(n);
oct.push_str(it.by_ref().take(2).collect::<String>().as_str());
let oct = oct;
match (oct.len(), u16::from_str_radix(&oct, 8)) {
(3, Ok(char_num)) if char_num <= 255 => {
cur_escape.push(char_num as u8);
match std::str::from_utf8(&cur_escape) {
Ok(s) => {
result.push_str(s);
cur_escape.clear();
}
Err(e) if e.error_len().is_none() => {}
Err(_) => {
return Err(format!(
"invalid escaped UTF-8 code point '{}'",
to_hex_string(&cur_escape)
))
}
}
}
_ => return Err(format!("invalid character escape: '\\{oct}'")),
}
}
'u' => {
let hex: String = it.by_ref().take(4).collect();
match (hex.len(), u32::from_str_radix(&hex, 16).map(char::from_u32)) {
(4, Ok(Some(ch))) => result.push(ch),
_ => return Err(format!("invalid character escape: '\\u{hex}'")),
}
}
'U' => {
let hex: String = it.by_ref().take(8).collect();
match (hex.len(), u32::from_str_radix(&hex, 16).map(char::from_u32)) {
(8, Ok(Some(ch))) => result.push(ch),
_ => return Err(format!("invalid character escape: '\\U{hex}'")),
}
}
ch => {
result.push('\\');
result.push(ch);
}
}
} else {
result.push(ch);
}
}
Ok(result)
}
fn to_hex_string(bytes: &[u8]) -> String {
bytes
.iter()
.fold(String::with_capacity(4 * bytes.len()), |mut acc, n| {
let _ = write!(acc, "\\x{n:02x}");
acc
})
}
pub fn parse(content: &str) -> Result<SystemdIni, ParseError> {
let mut parser = Parser {
content: content.chars().peekable(),
line: 0,
column: 0,
};
let mut sections = BTreeMap::new();
while let Some((key, entries)) = parser.parse_section()? {
sections.insert(key, entries);
}
Ok(SystemdIni(sections))
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn test_data_path(file_name: &str) -> PathBuf {
PathBuf::from("testdata/unit_file").join(file_name)
}
fn systemd_ini<S: Into<Sections>>(sections: S) -> SystemdIni {
SystemdIni(sections.into())
}
fn section<M: Into<Entries>>(name: &str, entries: M) -> (String, Entries) {
(name.to_string(), entries.into())
}
fn entry(key: &str, value: Value) -> (String, Value) {
(key.to_string(), value)
}
fn entry_str(key: &str, value: &str) -> (String, Value) {
entry(key, Value::Strings(vec![value.to_string()]))
}
fn entry_bool(key: &str, value: bool) -> (String, Value) {
entry(key, Value::Bool(value))
}
#[test]
fn can_remove_with_empty_section() {
let mut actual = systemd_ini([section("Section", [])]);
let expected = actual.clone();
actual.remove("Missing", "missing");
assert_eq!(actual, expected);
}
#[test]
fn can_remove_with_missing_key() {
let mut actual = systemd_ini([section("Section", [entry_str("entry", "value")])]);
let expected = actual.clone();
actual.remove("Section", "missing");
assert_eq!(actual, expected);
}
#[test]
fn can_remove_with_existing_key() {
let mut actual = systemd_ini([section(
"Section",
[entry_str("entry", "value"), entry_str("existing", "value")],
)]);
let expected = systemd_ini([section("Section", [entry_str("entry", "value")])]);
actual.remove("Section", "existing");
assert_eq!(actual, expected);
}
#[test]
fn can_extend_empty_with_non_empty() {
let mut actual = systemd_ini([]);
let mergee = systemd_ini([section("Section", [entry_str("entry", "value")])]);
let expected = mergee.clone();
actual.extend(mergee);
assert_eq!(actual, expected);
}
#[test]
fn can_extend_non_empty_with_empty() {
let mut actual = systemd_ini([section("Section", [entry_str("entry", "value")])]);
let mergee = systemd_ini([]);
let expected = actual.clone();
actual.extend(mergee);
assert_eq!(actual, expected);
}
#[test]
fn can_extend_non_empty_section_with_empty_section() {
let mut actual = systemd_ini([section("Section", [entry_str("entry", "value")])]);
let mergee = systemd_ini([section("Section", [])]);
let expected = actual.clone();
actual.extend(mergee);
assert_eq!(actual, expected);
}
#[test]
fn can_extend_empty_section_with_non_empty_section() {
let mut actual = systemd_ini([section("Section", [])]);
let mergee = systemd_ini([section("Section", [entry_str("entry", "value")])]);
let expected = mergee.clone();
actual.extend(mergee);
assert_eq!(actual, expected);
}
#[test]
fn can_extend_non_empty_section_with_non_empty_section() {
let mut actual = systemd_ini([section(
"Section",
[entry_str("entry1", "value1"), entry_str("entry2", "value2")],
)]);
let mergee = systemd_ini([section(
"Section",
[entry_str("entry1", "value2"), entry_str("entry3", "value3")],
)]);
let expected = systemd_ini([section(
"Section",
[
entry_str("entry1", "value2"),
entry_str("entry2", "value2"),
entry_str("entry3", "value3"),
],
)]);
actual.extend(mergee);
assert_eq!(actual, expected);
}
#[test]
fn can_eq_excluding_nothing() {
let a = systemd_ini([section(
"Section",
[entry_str("entry1", "value1"), entry_str("entry2", "value2")],
)]);
let same = systemd_ini([section(
"Section",
[entry_str("entry1", "value1"), entry_str("entry2", "value2")],
)]);
let different_value = systemd_ini([section(
"Section",
[
entry_str("entry1", "value1"),
entry_str("entry2", "value2'"),
],
)]);
let different_entry = systemd_ini([section(
"Section",
[entry_str("entry1", "value1"), entry_str("entry3", "value3")],
)]);
assert!(a.eq_excluding(&same, &[]));
assert!(!a.eq_excluding(&different_value, &[]));
assert!(!a.eq_excluding(&different_entry, &[]));
}
#[test]
fn can_eq_excluding_one() {
let a = systemd_ini([section(
"Section",
[entry_str("entry1", "value1"), entry_str("entry2", "value2")],
)]);
let same = systemd_ini([section(
"Section",
[entry_str("entry1", "value1"), entry_str("entry2", "value2")],
)]);
let different_value = systemd_ini([section(
"Section",
[
entry_str("entry1", "value1"),
entry_str("entry2", "value2'"),
],
)]);
let different_entry = systemd_ini([section(
"Section",
[entry_str("entry1", "value1"), entry_str("entry3", "value3")],
)]);
assert!(a.eq_excluding(&same, &[("Section", "entry2")]));
assert!(a.eq_excluding(&different_value, &[("Section", "entry2")]));
assert!(!a.eq_excluding(&different_entry, &[("Section", "entry2")]));
}
#[test]
fn can_parse_empty_file() {
let actual = parse("").unwrap();
let expected = systemd_ini([]);
assert_eq!(actual, expected);
}
#[test]
fn can_parse_file_with_only_comments() {
let actual = parse(
r#"
# comment1
; comment2
;
# comment3
"#,
)
.unwrap();
let expected = systemd_ini([]);
assert_eq!(actual, expected);
}
#[test]
fn can_parse_empty_section() {
let actual = parse(
r#"
[abcde]
"#,
)
.unwrap();
let expected = systemd_ini([section("abcde", [])]);
assert_eq!(actual, expected);
}
#[test]
fn can_parse_file_with_comment_on_last_line() {
let actual = parse(
r#"
[abcde]
foo = bar
# comment1
; comment2"#,
)
.unwrap();
let expected = systemd_ini([section("abcde", [entry_str("foo", "bar")])]);
assert_eq!(actual, expected);
}
#[test]
fn can_parse_section() {
let ini_no_new_line = r#"
[abcde]
foo = bar
baz = boo"#;
let ini_new_line = {
let mut s = String::from(ini_no_new_line);
s.push('\n');
s
};
let actual_no_new_line = parse(ini_no_new_line).unwrap();
let actual_new_line = parse(&ini_new_line).unwrap();
let expected = systemd_ini([section(
"abcde",
[entry_str("foo", "bar"), entry_str("baz", "boo")],
)]);
assert_eq!(actual_no_new_line, expected);
assert_eq!(actual_new_line, expected);
}
#[test]
fn fails_for_entry_without_equals() {
let actual = parse(
r#"
[abcde]
foo bar
"#,
);
let expected = Err(ParseError {
message: "expected '=' but got 'b'".into(),
line: 2,
column: 4,
});
assert_eq!(actual, expected);
assert_eq!(
format!("{}", actual.unwrap_err()),
"expected '=' but got 'b' at line 2, column 4"
);
}
#[test]
fn fails_for_entry_without_key() {
let actual = parse(
r#"
[abcde]
= bar
"#,
);
let expected = Err(ParseError {
message: "expected entry key name but got '='".into(),
line: 2,
column: 1,
});
assert_eq!(actual, expected);
assert_eq!(
format!("{}", actual.unwrap_err()),
"expected entry key name but got '=' at line 2, column 1"
);
}
#[test]
fn fails_for_entry_with_bad_boolean() {
let actual = parse(
r#"
[Unit]
RefuseManualStart = nonsense
"#,
);
let expected = Err(ParseError {
message: "invalid Boolean value \"nonsense\"".into(),
line: 3,
column: 0,
});
assert_eq!(actual, expected);
assert_eq!(
format!("{}", actual.unwrap_err()),
"invalid Boolean value \"nonsense\" at line 3, column 0"
);
}
#[test]
fn fails_for_entry_with_bad_unit_switch_method() {
let actual = parse(
r#"
[Unit]
X-SwitchMethod = nonsense
"#,
);
let expected = Err(ParseError {
message: "unknown unit switch method \"nonsense\"".into(),
line: 3,
column: 0,
});
assert_eq!(actual, expected);
assert_eq!(
format!("{}", actual.unwrap_err()),
"unknown unit switch method \"nonsense\" at line 3, column 0"
);
}
#[test]
fn can_parse_section_with_list() {
let actual = parse(
r#"
[abcde]
foo = bar
foo = baz
"#,
)
.unwrap();
let expected = systemd_ini([section(
"abcde",
[entry(
"foo",
Value::Strings(vec!["bar".to_string(), "baz".to_string()]),
)],
)]);
assert_eq!(actual, expected);
}
#[test]
fn can_parse_section_with_line_continuation() {
let actual1 = parse(
r#"
[abcde]
foo = bar\
baz
"#,
)
.unwrap();
let actual2 = parse(
r#"
[abcde]
foo = bar\
# An intervening comment should be ignored.
; This comment should also be ignored.
baz
"#,
)
.unwrap();
let expected = systemd_ini([section("abcde", [entry_str("foo", "bar baz")])]);
assert_eq!(actual1, expected);
assert_eq!(actual2, expected);
}
#[test]
fn can_unescape_strings() {
assert_eq!(to_unescaped_string("plain"), Ok("plain".to_string()));
assert_eq!(to_unescaped_string("<\\a>"), Ok("<\x07>".to_string()));
assert_eq!(to_unescaped_string("<\\b>"), Ok("<\x08>".to_string()));
assert_eq!(to_unescaped_string("<\\f>"), Ok("<\x0C>".to_string()));
assert_eq!(to_unescaped_string("<\\n>"), Ok("<\n>".to_string()));
assert_eq!(to_unescaped_string("<\\r>"), Ok("<\r>".to_string()));
assert_eq!(to_unescaped_string("<\\t>"), Ok("<\t>".to_string()));
assert_eq!(to_unescaped_string("<\\v>"), Ok("<\x0B>".to_string()));
assert_eq!(to_unescaped_string("<\\\\>"), Ok("<\\>".to_string()));
assert_eq!(to_unescaped_string("<\\\">"), Ok("<\">".to_string()));
assert_eq!(to_unescaped_string("<\\'>"), Ok("<\'>".to_string()));
assert_eq!(to_unescaped_string("<\\s>"), Ok("< >".to_string()));
assert_eq!(to_unescaped_string("<\\x52>"), Ok("<R>".to_string()));
assert_eq!(to_unescaped_string("<\\122>"), Ok("<R>".to_string()));
assert_eq!(to_unescaped_string("<\\u0052>"), Ok("<R>".to_string()));
assert_eq!(to_unescaped_string("<\\U00000052>"), Ok("<R>".to_string()));
assert_eq!(to_unescaped_string("<\\h>"), Ok("<\\h>".to_string()));
assert_eq!(
to_unescaped_string("<\\x1>"),
Err("invalid character escape: '\\x1>'".into())
);
assert_eq!(
to_unescaped_string("<\\199>"),
Err("invalid character escape: '\\199'".into())
);
assert_eq!(
to_unescaped_string("<\\u123>"),
Err("invalid character escape: '\\u123>'".into())
);
assert_eq!(
to_unescaped_string("<\\U1234>"),
Err("invalid character escape: '\\U1234>'".into())
);
assert_eq!(
to_unescaped_string("<\\"),
Err("invalid character escape: '\\'".into())
);
assert_eq!(
to_unescaped_string("<\\xf0\\xff>"),
Err("invalid escaped UTF-8 code point '\\xf0\\xff'".into())
);
assert_eq!(
to_unescaped_string("<\\360\\377>"),
Err("invalid escaped UTF-8 code point '\\xf0\\xff'".into())
);
}
#[test]
fn can_parse_complicated_systemd_unit() {
let content = std::fs::read_to_string(test_data_path("escaped-values.service")).unwrap();
let actual = parse(&content).unwrap();
let expected = systemd_ini([
section("Install", [entry_str("WantedBy", "default.target")]),
section(
"Service",
[
entry_str(
"ExecStart",
"/bin/sh -c 'systemd\\x2Dnotify READY=1; /run/current\\x2dsystem/sw/bin/sleep 10s'",
),
entry_str("NotifyAccess", "all"),
entry_str("Type", "notify"),
],
),
section(
"Unit",
[
entry_str(
"Description",
"Successful simple service with X-RestartIfChanged = false\\x20\\xf0\\x9f\\x99\\x82",
),
entry_bool("RefuseManualStart", false),
entry_bool("RefuseManualStop", true),
entry("X-SwitchMethod", Value::SwitchMethod(UnitSwitchMethod::Restart)),
],
),
]);
assert_eq!(actual, expected);
}
#[test]
fn can_parse_upstream_systemd_unit() {
let content =
std::fs::read_to_string(test_data_path("systemd-tmpfiles-setup.service")).unwrap();
let actual = parse(&content).unwrap();
let expected = systemd_ini([
section("Install", [entry_str("WantedBy", "basic.target")]),
section(
"Service",
[
entry_str("Type", "oneshot"),
entry_str(
"ExecStart",
"systemd-tmpfiles --user --create --remove --boot",
),
entry_str("RemainAfterExit", "yes"),
entry_str("SuccessExitStatus", "DATAERR"),
],
),
section(
"Unit",
[
entry_str("Description", "Create User Files and Directories"),
entry_str("Documentation", "man:tmpfiles.d(5) man:systemd-tmpfiles(8)"),
entry_str("DefaultDependencies", "no"),
entry_str("Conflicts", "shutdown.target"),
entry_str("Before", "basic.target shutdown.target"),
entry_bool("RefuseManualStop", true),
],
),
]);
assert_eq!(actual, expected);
}
}