2023-01-01 18:54:57 -06:00
use std ::fmt ;
2023-09-14 04:12:34 -05:00
use std ::{ collections ::BTreeMap , str ::FromStr } ;
2023-01-01 18:54:57 -06:00
2023-12-28 10:03:02 -06:00
use anyhow ::Result ;
2023-01-01 18:54:57 -06:00
2023-07-22 13:49:46 -05:00
use crate ::generation ::Generation ;
2023-01-01 18:54:57 -06:00
/// An os-release file represented by a BTreeMap.
///
/// This is implemented using a map, so that it can be easily extended in the future (e.g. by
/// reading the original os-release and patching it).
///
/// The BTreeMap is used over a HashMap, so that the keys are ordered. This is irrelevant for
/// systemd-boot (which does not care about order when reading the os-release file) but is useful
/// for testing. Ordered keys allow using snapshot tests.
2023-01-17 18:58:45 -06:00
pub struct OsRelease ( pub BTreeMap < String , String > ) ;
2023-01-01 18:54:57 -06:00
impl OsRelease {
pub fn from_generation ( generation : & Generation ) -> Result < Self > {
let mut map = BTreeMap ::new ( ) ;
// Because of a null pointer dereference, `bootctl` segfaults when no ID field is present
// in the .osrel section of the stub.
// Fixed in https://github.com/systemd/systemd/pull/25953
//
// Because the ID field here does not have the same meaning as in a real os-release file,
// it is fine to use a dummy value.
2023-01-17 18:58:45 -06:00
map . insert ( " ID " . into ( ) , String ::from ( " lanza " ) ) ;
2023-10-20 04:29:28 -05:00
// systemd-boot will only show VERSION_ID when PRETTY_NAME is not unique. This is
// confusing to users. Make sure that our PRETTY_NAME is unique, so we get a consistent
// user experience.
//
// See #220.
2023-05-17 14:40:03 -05:00
map . insert (
" PRETTY_NAME " . into ( ) ,
2023-10-20 04:29:28 -05:00
format! (
" {} ({}) " ,
generation . spec . bootspec . bootspec . label ,
generation . describe ( )
) ,
2023-05-17 14:40:03 -05:00
) ;
2023-10-20 04:29:28 -05:00
2023-02-08 17:19:33 -06:00
map . insert ( " VERSION_ID " . into ( ) , generation . describe ( ) ) ;
2023-01-01 18:54:57 -06:00
Ok ( Self ( map ) )
}
2023-09-14 04:12:34 -05:00
}
2023-01-17 18:58:45 -06:00
2023-09-14 04:12:34 -05:00
impl FromStr for OsRelease {
type Err = anyhow ::Error ;
2023-01-17 18:58:45 -06:00
/// Parse the string representation of a os-release file.
///
/// **Beware before reusing this function!**
///
/// This parser might not parse all valid os-release files correctly. It is only designed to
/// read the `VERSION` key from the os-release of a systemd-boot binary.
2023-09-14 04:12:34 -05:00
fn from_str ( value : & str ) -> Result < Self > {
2023-01-17 18:58:45 -06:00
let mut map = BTreeMap ::new ( ) ;
2023-12-28 10:03:02 -06:00
enum State {
PreKey ,
Key ,
PreValue ,
Value ,
ValueEscape ,
SingleQuoteValue ,
DoubleQuoteValue ,
DoubleQuoteValueEscape ,
Comment ,
CommentEscape ,
}
use State ::* ;
let mut state = State ::PreKey ;
let mut current_key = String ::new ( ) ;
let mut current_value = String ::new ( ) ;
const COMMENTS : & str = " #; " ;
const WHITESPACE : & str = " \t \n \r " ;
const NEWLINE : & str = " \r \n " ;
const SHELL_NEED_ESCAPE : & str = " \" \\ `$ " ;
for c in value . chars ( ) {
match state {
PreKey = > {
if COMMENTS . contains ( c ) {
state = Comment ;
} else if ! WHITESPACE . contains ( c ) {
state = Key ;
current_key . push ( c ) ;
}
}
Key = > {
if NEWLINE . contains ( c ) {
// keys without any '=' are simply ignored
state = PreKey ;
current_key . clear ( ) ;
} else if c = = '=' {
state = PreValue ;
} else {
current_key . push ( c ) ;
}
}
PreValue = > {
if NEWLINE . contains ( c ) {
state = PreKey ;
// strip trailing whitespace from key
let key = current_key . trim_end ( ) . to_owned ( ) ;
map . insert ( key , current_value . clone ( ) ) ;
current_key . clear ( ) ;
current_value . clear ( ) ;
} else if c = = '\'' {
state = SingleQuoteValue ;
} else if c = = '"' {
state = DoubleQuoteValue ;
} else if c = = '\\' {
state = ValueEscape ;
} else if ! WHITESPACE . contains ( c ) {
state = Value ;
current_value . push ( c ) ;
}
}
Value = > {
if NEWLINE . contains ( c ) {
state = PreKey ;
// strip trailing whitespace from key
let key = current_key . trim_end ( ) . to_owned ( ) ;
// strip trailing whitespace from value
let value = current_value . trim_end ( ) . to_owned ( ) ;
map . insert ( key , value ) ;
current_key . clear ( ) ;
current_value . clear ( ) ;
} else if c = = '\\' {
state = ValueEscape ;
} else {
current_value . push ( c ) ;
}
}
ValueEscape = > {
state = Value ;
if ! NEWLINE . contains ( c ) {
// Escaped newlines we eat up entirely
current_value . push ( c ) ;
}
}
SingleQuoteValue = > {
if c = = '\'' {
state = PreValue ;
} else {
current_value . push ( c ) ;
}
}
DoubleQuoteValue = > {
if c = = '"' {
state = PreValue ;
} else if c = = '\\' {
state = DoubleQuoteValueEscape ;
} else {
current_value . push ( c ) ;
}
}
DoubleQuoteValueEscape = > {
state = DoubleQuoteValue ;
if SHELL_NEED_ESCAPE . contains ( c ) {
// If this is a char that needs escaping, just unescape it.
current_value . push ( c ) ;
} else if c ! = '\n' {
// If other char than what needs escaping, keep the "\"
// in place, like the real shell does.
current_value . push ( '\\' ) ;
current_value . push ( c ) ;
}
// Escaped newlines (aka "continuation lines") are eaten up entirely
}
Comment = > {
if c = = '\\' {
state = CommentEscape ;
} else if NEWLINE . contains ( c ) {
state = PreKey ;
}
}
CommentEscape = > {
log ::debug! ( " The line which doesn't begin with \" ; \" or \" # \" , but follows a comment line trailing with escape is now treated as a non comment line since v254. " ) ;
if NEWLINE . contains ( c ) {
state = PreKey ;
} else {
state = Comment ;
}
}
}
}
if matches! (
state ,
PreValue
| Value
| ValueEscape
| SingleQuoteValue
| DoubleQuoteValue
| DoubleQuoteValueEscape
) {
// strip trailing whitespace from key
let key = current_key . trim_end ( ) . to_owned ( ) ;
let value = if matches! ( state , Value ) {
// strip trailing whitespace from value
current_value . trim_end ( ) . to_owned ( )
} else {
current_value
} ;
map . insert ( key , value ) ;
2023-01-17 18:58:45 -06:00
}
Ok ( Self ( map ) )
}
2023-01-01 18:54:57 -06:00
}
/// Display OsRelease in the format of an os-release file.
impl fmt ::Display for OsRelease {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
for ( key , value ) in & self . 0 {
writeln! ( f , " {}={} " , key , value ) ?
}
Ok ( ( ) )
}
}
2023-01-17 18:58:45 -06:00
#[ cfg(test) ]
mod tests {
use super ::* ;
use std ::ffi ::CStr ;
#[ test ]
fn parses_correctly_from_str ( ) -> Result < ( ) > {
let os_release_cstr = CStr ::from_bytes_with_nul ( b " ID=systemd-boot \n VERSION= \" 252.1 \" \n \0 " ) ? ;
let os_release_str = os_release_cstr . to_str ( ) ? ;
let os_release = OsRelease ::from_str ( os_release_str ) ? ;
assert! ( os_release . 0 [ " ID " ] = = " systemd-boot " ) ;
assert! ( os_release . 0 [ " VERSION " ] = = " 252.1 " ) ;
Ok ( ( ) )
}
2023-01-30 04:46:48 -06:00
#[ test ]
fn escaping_works ( ) -> Result < ( ) > {
let teststring = r #"
NO_QUOTES = systemd - boot
DOUBLE_QUOTES = " systemd-boot "
SINGLE_QUOTES = ' systemd - boot '
UNESCAPED_DOLLAR = $ 1.2
ESCAPED_DOLLAR = \ $ 1.2
UNESCAPED_BACKTICK = ` 1.2
ESCAPED_BACKTICK = \ ` 1.2
UNESCAPED_QUOTE = " " 1.2 "
ESCAPED_QUOTE = \ " 1.2
" #;
let os_release = OsRelease ::from_str ( teststring ) ? ;
assert! ( os_release . 0 [ " NO_QUOTES " ] = = " systemd-boot " ) ;
assert! ( os_release . 0 [ " DOUBLE_QUOTES " ] = = " systemd-boot " ) ;
assert! ( os_release . 0 [ " SINGLE_QUOTES " ] = = " systemd-boot " ) ;
assert! ( os_release . 0 [ " UNESCAPED_DOLLAR " ] = = " $1.2 " ) ;
assert! ( os_release . 0 [ " ESCAPED_DOLLAR " ] = = " $1.2 " ) ;
assert! ( os_release . 0 [ " UNESCAPED_BACKTICK " ] = = " `1.2 " ) ;
assert! ( os_release . 0 [ " ESCAPED_BACKTICK " ] = = " `1.2 " ) ;
2023-12-28 10:03:02 -06:00
assert! ( os_release . 0 [ " UNESCAPED_QUOTE " ] = = " 1.2 \" " ) ;
2023-01-30 04:46:48 -06:00
assert! ( os_release . 0 [ " ESCAPED_QUOTE " ] = = " \" 1.2 " ) ;
Ok ( ( ) )
}
2023-01-17 18:58:45 -06:00
}