grouping.rs (7798B)
1 // grouping 2 3 // @<mtime> 4 // :sysboot 5 // procfs,modules,devfs 6 // run,clock,sysfs 7 // hostname,hwdrivers,mdev,rootfs 8 // swap 9 // localmount 10 // syslog,seed 11 // :boot 12 // :default 13 // getty@tty1 14 15 use std::collections::HashMap; 16 use std::fmt; 17 use std::fmt::Formatter; 18 use std::str::FromStr; 19 20 use base64::Engine; 21 use base64::prelude::BASE64_STANDARD; 22 use nom::branch::alt; 23 use nom::bytes::complete::is_a; 24 use nom::character::complete::{alpha1, alphanumeric1, char, newline, not_line_ending}; 25 use nom::combinator::{all_consuming, map, map_res, opt, recognize}; 26 use nom::error::Error; 27 use nom::multi::{many0, separated_list0, separated_list1}; 28 use nom::sequence::{delimited, pair, preceded, terminated}; 29 use nom::{Finish, IResult}; 30 31 use crate::UnitName; 32 33 type Sha256Hash = [u8; 32]; 34 35 #[derive(Debug, PartialEq, Eq, Clone)] 36 pub struct DependencyGrouping { 37 pub hash: Sha256Hash, 38 pub groups: HashMap<Box<str>, Vec<Vec<UnitName>>>, 39 } 40 41 impl fmt::Display for DependencyGrouping { 42 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 43 writeln!(f, "@{}", BASE64_STANDARD.encode(self.hash))?; 44 45 for (name, levels) in self.groups.iter() { 46 writeln!(f, ":{name}")?; 47 48 for level in levels.iter() { 49 writeln!(f, "{}", level.join(","))?; 50 } 51 } 52 53 Ok(()) 54 } 55 } 56 57 impl FromStr for DependencyGrouping { 58 type Err = Error<String>; 59 60 fn from_str(s: &str) -> Result<Self, Self::Err> { 61 match grouping(s).finish() { 62 Ok((_remaining, grouping)) => Ok(grouping), 63 Err(e) => Err(Error { 64 input: e.input.to_string(), 65 code: e.code, 66 }), 67 } 68 } 69 } 70 71 fn grouping(input: &str) -> IResult<&str, DependencyGrouping> { 72 all_consuming(map( 73 pair(header, separated_list1(newline, group)), 74 |(hash, groups)| DependencyGrouping { 75 hash, 76 groups: groups.into_iter().collect(), 77 }, 78 ))(input) 79 } 80 81 fn header(input: &str) -> IResult<&str, Sha256Hash> { 82 delimited(char('@'), map_res(not_line_ending, hash), newline)(input) 83 } 84 85 fn hash(input: &str) -> Result<Sha256Hash, base64::DecodeSliceError> { 86 let mut buff: Sha256Hash = [0; 32]; 87 88 BASE64_STANDARD.decode_slice(input, &mut buff)?; 89 90 Ok(buff) 91 } 92 93 fn group(input: &str) -> IResult<&str, (Box<str>, Vec<Vec<UnitName>>)> { 94 pair( 95 terminated(group_name, opt(newline)), 96 separated_list0(newline, level), 97 )(input) 98 } 99 100 fn group_name(input: &str) -> IResult<&str, Box<str>> { 101 preceded( 102 char(':'), 103 map( 104 recognize(pair(alpha1, many0(alt((alphanumeric1, is_a("-_")))))), 105 Box::from, 106 ), 107 )(input) 108 } 109 110 fn level(input: &str) -> IResult<&str, Vec<UnitName>> { 111 separated_list1(char(','), unit_name)(input) 112 } 113 114 fn unit_name(input: &str) -> IResult<&str, UnitName> { 115 map( 116 recognize(pair(alpha1, many0(alt((alphanumeric1, is_a("-_.@")))))), 117 UnitName::from, 118 )(input) 119 } 120 121 #[cfg(test)] 122 mod tests { 123 use crate::parser_test; 124 125 use super::*; 126 127 // TODO; we should have negative tests 128 #[test] 129 fn parse_unit_name() -> Result<(), Error<&'static str>> { 130 parser_test!( 131 unit_name, 132 [ 133 "foo" => UnitName::from("foo"), 134 "foo2" => UnitName::from("foo2"), 135 "foo_bar" => UnitName::from("foo_bar"), 136 "foo-bar" => UnitName::from("foo-bar"), 137 "foo@bar" => UnitName::from("foo@bar") 138 ] 139 ); 140 141 Ok(()) 142 } 143 144 #[test] 145 fn parse_level() -> Result<(), Error<&'static str>> { 146 parser_test!( 147 level, 148 [ 149 "foo,bar,baz" => vec![ 150 UnitName::from("foo"), 151 UnitName::from("bar"), 152 UnitName::from("baz"), 153 ], 154 "foo" => vec![ 155 UnitName::from("foo") 156 ] 157 ] 158 ); 159 160 Ok(()) 161 } 162 163 #[test] 164 fn parse_group_name() -> Result<(), Error<&'static str>> { 165 parser_test!( 166 group_name, 167 [ 168 ":foo" => Box::from("foo"), 169 ":foo2" => Box::from("foo2"), 170 ":foo_bar" => Box::from("foo_bar"), 171 ":foo-bar" => Box::from("foo-bar") 172 ] 173 ); 174 175 Ok(()) 176 } 177 178 #[test] 179 fn parse_group() -> Result<(), Error<&'static str>> { 180 parser_test!( 181 group, 182 [ 183 ":foo" => (Box::from("foo"), vec![]), 184 ":foo\nbar,baz" => ( 185 Box::from("foo"), 186 vec![vec![UnitName::from("bar"), UnitName::from("baz")]] 187 ) 188 ] 189 ); 190 191 Ok(()) 192 } 193 194 #[test] 195 fn parse_header() -> Result<(), Error<&'static str>> { 196 parser_test!( 197 header, 198 [ 199 "@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n" => [0; 32], 200 "@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\n" => [1; 32] 201 ] 202 ); 203 204 Ok(()) 205 } 206 207 #[test] 208 fn parse_grouping() -> Result<(), Error<&'static str>> { 209 parser_test!( 210 grouping, 211 [ 212 "@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n:sysboot\nprocfs,modules\nrun" => DependencyGrouping { 213 hash: [0; 32], 214 groups: HashMap::from([ 215 ( 216 Box::from("sysboot"), 217 vec![ 218 vec![UnitName::from("procfs"), UnitName::from("modules")], 219 vec![UnitName::from("run")] 220 ] 221 ) 222 ]) 223 }, 224 "@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\n:sysboot\nprocfs,modules\nrun\n:default\ngetty@tty1" => DependencyGrouping { 225 hash: [1; 32], 226 groups: HashMap::from([ 227 ( 228 Box::from("sysboot"), 229 vec![ 230 vec![UnitName::from("procfs"), UnitName::from("modules")], 231 vec![UnitName::from("run")] 232 ] 233 ), 234 ( 235 Box::from("default"), 236 vec![ 237 vec![UnitName::from("getty@tty1")] 238 ] 239 ) 240 ]) 241 } 242 ] 243 ); 244 245 Ok(()) 246 } 247 248 #[test] 249 fn display_grouping() { 250 let grouping = DependencyGrouping { 251 hash: [2; 32], 252 groups: HashMap::from([ 253 ( 254 Box::from("sysboot"), 255 vec![ 256 vec![UnitName::from("procfs"), UnitName::from("modules")], 257 vec![UnitName::from("run")], 258 ], 259 ), 260 ( 261 Box::from("default"), 262 vec![ 263 vec![UnitName::from("syslog")], 264 vec![UnitName::from("getty@tty1")], 265 ], 266 ), 267 ]), 268 }; 269 270 // hashmap ordering leads to 2 possible orderings 271 let output = grouping.to_string(); 272 273 match output.as_str() { 274 "@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=\n:default\nsyslog\ngetty@tty1\n:sysboot\nprocfs,modules\nrun\n" 275 | "@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=\n:sysboot\nprocfs,modules\nrun\n:default\nsyslog\ngetty@tty1\n" => 276 {} 277 s => panic!("did not expect {s}"), 278 } 279 } 280 }