1 /*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 use aconfig_protos::{
18 ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoTracepoint,
19 };
20 use aconfig_protos::{ProtoParsedFlag, ProtoParsedFlags};
21 use anyhow::{anyhow, bail, Context, Result};
22 use protobuf::Message;
23
24 #[derive(Clone, Debug, PartialEq, Eq)]
25 pub enum DumpFormat {
26 Protobuf,
27 Textproto,
28 Custom(String),
29 }
30
31 impl TryFrom<&str> for DumpFormat {
32 type Error = anyhow::Error;
33
try_from(value: &str) -> std::result::Result<Self, Self::Error>34 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
35 match value {
36 // protobuf formats
37 "protobuf" => Ok(Self::Protobuf),
38 "textproto" => Ok(Self::Textproto),
39 // custom format
40 _ => Ok(Self::Custom(value.to_owned())),
41 }
42 }
43 }
44
dump_parsed_flags<I>(parsed_flags_iter: I, format: DumpFormat) -> Result<Vec<u8>> where I: Iterator<Item = ProtoParsedFlag>,45 pub fn dump_parsed_flags<I>(parsed_flags_iter: I, format: DumpFormat) -> Result<Vec<u8>>
46 where
47 I: Iterator<Item = ProtoParsedFlag>,
48 {
49 let mut output = Vec::new();
50 match format {
51 DumpFormat::Protobuf => {
52 let parsed_flags =
53 ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() };
54 parsed_flags.write_to_vec(&mut output)?;
55 }
56 DumpFormat::Textproto => {
57 let parsed_flags =
58 ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() };
59 let s = protobuf::text_format::print_to_string_pretty(&parsed_flags);
60 output.extend_from_slice(s.as_bytes());
61 }
62 DumpFormat::Custom(format) => {
63 for flag in parsed_flags_iter {
64 dump_custom_format(&flag, &format, &mut output);
65 }
66 }
67 }
68 Ok(output)
69 }
70
dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec<u8>)71 fn dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec<u8>) {
72 fn format_trace(trace: &[ProtoTracepoint]) -> String {
73 trace
74 .iter()
75 .map(|tracepoint| {
76 format!(
77 "{}: {:?} + {:?}",
78 tracepoint.source(),
79 tracepoint.permission(),
80 tracepoint.state()
81 )
82 })
83 .collect::<Vec<_>>()
84 .join(", ")
85 }
86
87 fn format_trace_paths(trace: &[ProtoTracepoint]) -> String {
88 trace.iter().map(|tracepoint| tracepoint.source()).collect::<Vec<_>>().join(", ")
89 }
90
91 fn format_metadata(metadata: &ProtoFlagMetadata) -> String {
92 format!("{:?}", metadata.purpose())
93 }
94
95 let mut str = format
96 // ProtoParsedFlag fields
97 .replace("{package}", flag.package())
98 .replace("{name}", flag.name())
99 .replace("{namespace}", flag.namespace())
100 .replace("{description}", flag.description())
101 .replace("{bug}", &flag.bug.join(", "))
102 .replace("{state}", &format!("{:?}", flag.state()))
103 .replace("{state:bool}", &format!("{}", flag.state() == ProtoFlagState::ENABLED))
104 .replace("{permission}", &format!("{:?}", flag.permission()))
105 .replace("{trace}", &format_trace(&flag.trace))
106 .replace("{trace:paths}", &format_trace_paths(&flag.trace))
107 .replace("{is_fixed_read_only}", &format!("{}", flag.is_fixed_read_only()))
108 .replace("{is_exported}", &format!("{}", flag.is_exported()))
109 .replace("{container}", flag.container())
110 .replace("{metadata}", &format_metadata(&flag.metadata))
111 // ParsedFlagExt functions
112 .replace("{fully_qualified_name}", &flag.fully_qualified_name());
113 str.push('\n');
114 output.extend_from_slice(str.as_bytes());
115 }
116
117 pub type DumpPredicate = dyn Fn(&ProtoParsedFlag) -> bool;
118
create_filter_predicate(filter: &str) -> Result<Box<DumpPredicate>>119 pub fn create_filter_predicate(filter: &str) -> Result<Box<DumpPredicate>> {
120 let predicates = filter
121 .split('+')
122 .map(|sub_filter| create_filter_predicate_single(sub_filter))
123 .collect::<Result<Vec<_>>>()?;
124 Ok(Box::new(move |flag| predicates.iter().all(|p| p(flag))))
125 }
126
create_filter_predicate_single(filter: &str) -> Result<Box<DumpPredicate>>127 fn create_filter_predicate_single(filter: &str) -> Result<Box<DumpPredicate>> {
128 fn enum_from_str<T>(expected: &[T], s: &str) -> Result<T>
129 where
130 T: std::fmt::Debug + Copy,
131 {
132 for candidate in expected.iter() {
133 if s == format!("{:?}", candidate) {
134 return Ok(*candidate);
135 }
136 }
137 let expected =
138 expected.iter().map(|state| format!("{:?}", state)).collect::<Vec<_>>().join(", ");
139 bail!("\"{s}\": not a valid flag state, expected one of {expected}");
140 }
141
142 let error_msg = format!("\"{filter}\": filter syntax error");
143 let (what, arg) = filter.split_once(':').ok_or_else(|| anyhow!(error_msg.clone()))?;
144 match what {
145 "package" => {
146 let expected = arg.to_owned();
147 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.package() == expected))
148 }
149 "name" => {
150 let expected = arg.to_owned();
151 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.name() == expected))
152 }
153 "namespace" => {
154 let expected = arg.to_owned();
155 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.namespace() == expected))
156 }
157 // description: not supported yet
158 "bug" => {
159 let expected = arg.to_owned();
160 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.bug.join(", ") == expected))
161 }
162 "state" => {
163 let expected = enum_from_str(&[ProtoFlagState::ENABLED, ProtoFlagState::DISABLED], arg)
164 .context(error_msg)?;
165 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.state() == expected))
166 }
167 "permission" => {
168 let expected = enum_from_str(
169 &[ProtoFlagPermission::READ_ONLY, ProtoFlagPermission::READ_WRITE],
170 arg,
171 )
172 .context(error_msg)?;
173 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.permission() == expected))
174 }
175 // trace: not supported yet
176 "is_fixed_read_only" => {
177 let expected: bool = arg.parse().context(error_msg)?;
178 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_fixed_read_only() == expected))
179 }
180 "is_exported" => {
181 let expected: bool = arg.parse().context(error_msg)?;
182 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_exported() == expected))
183 }
184 "container" => {
185 let expected = arg.to_owned();
186 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.container() == expected))
187 }
188 // metadata: not supported yet
189 "fully_qualified_name" => {
190 let expected = arg.to_owned();
191 Ok(Box::new(move |flag: &ProtoParsedFlag| flag.fully_qualified_name() == expected))
192 }
193 _ => Err(anyhow!(error_msg)),
194 }
195 }
196
197 #[cfg(test)]
198 mod tests {
199 use super::*;
200 use crate::test::parse_test_flags;
201 use aconfig_protos::ProtoParsedFlags;
202 use protobuf::Message;
203
parse_enabled_ro_flag() -> ProtoParsedFlag204 fn parse_enabled_ro_flag() -> ProtoParsedFlag {
205 parse_test_flags().parsed_flag.into_iter().find(|pf| pf.name() == "enabled_ro").unwrap()
206 }
207
208 #[test]
test_dumpformat_from_str()209 fn test_dumpformat_from_str() {
210 // supported format types
211 assert_eq!(DumpFormat::try_from("protobuf").unwrap(), DumpFormat::Protobuf);
212 assert_eq!(DumpFormat::try_from("textproto").unwrap(), DumpFormat::Textproto);
213 assert_eq!(
214 DumpFormat::try_from("foobar").unwrap(),
215 DumpFormat::Custom("foobar".to_owned())
216 );
217 }
218
219 #[test]
test_dump_parsed_flags_protobuf_format()220 fn test_dump_parsed_flags_protobuf_format() {
221 let expected = protobuf::text_format::parse_from_str::<ProtoParsedFlags>(
222 crate::test::TEST_FLAGS_TEXTPROTO,
223 )
224 .unwrap()
225 .write_to_bytes()
226 .unwrap();
227 let parsed_flags = parse_test_flags();
228 let actual =
229 dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Protobuf).unwrap();
230 assert_eq!(expected, actual);
231 }
232
233 #[test]
test_dump_parsed_flags_textproto_format()234 fn test_dump_parsed_flags_textproto_format() {
235 let parsed_flags = parse_test_flags();
236 let bytes =
237 dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Textproto).unwrap();
238 let text = std::str::from_utf8(&bytes).unwrap();
239 assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim());
240 }
241
242 #[test]
test_dump_parsed_flags_custom_format()243 fn test_dump_parsed_flags_custom_format() {
244 macro_rules! assert_dump_parsed_flags_custom_format_contains {
245 ($format:expr, $expected:expr) => {
246 let parsed_flags = parse_test_flags();
247 let bytes = dump_parsed_flags(
248 parsed_flags.parsed_flag.into_iter(),
249 $format.try_into().unwrap(),
250 )
251 .unwrap();
252 let text = std::str::from_utf8(&bytes).unwrap();
253 assert!(text.contains($expected));
254 };
255 }
256
257 // custom format
258 assert_dump_parsed_flags_custom_format_contains!(
259 "{fully_qualified_name}={permission} + {state}",
260 "com.android.aconfig.test.enabled_ro=READ_ONLY + ENABLED"
261 );
262 }
263
264 #[test]
test_dump_custom_format()265 fn test_dump_custom_format() {
266 macro_rules! assert_custom_format {
267 ($format:expr, $expected:expr) => {
268 let flag = parse_enabled_ro_flag();
269 let mut bytes = vec![];
270 dump_custom_format(&flag, $format, &mut bytes);
271 let text = std::str::from_utf8(&bytes).unwrap();
272 assert_eq!(text, $expected);
273 };
274 }
275
276 assert_custom_format!("{package}", "com.android.aconfig.test\n");
277 assert_custom_format!("{name}", "enabled_ro\n");
278 assert_custom_format!("{namespace}", "aconfig_test\n");
279 assert_custom_format!("{description}", "This flag is ENABLED + READ_ONLY\n");
280 assert_custom_format!("{bug}", "abc\n");
281 assert_custom_format!("{state}", "ENABLED\n");
282 assert_custom_format!("{state:bool}", "true\n");
283 assert_custom_format!("{permission}", "READ_ONLY\n");
284 assert_custom_format!("{trace}", "tests/test.aconfig: READ_WRITE + DISABLED, tests/first.values: READ_WRITE + DISABLED, tests/second.values: READ_ONLY + ENABLED\n");
285 assert_custom_format!(
286 "{trace:paths}",
287 "tests/test.aconfig, tests/first.values, tests/second.values\n"
288 );
289 assert_custom_format!("{is_fixed_read_only}", "false\n");
290 assert_custom_format!("{is_exported}", "false\n");
291 assert_custom_format!("{container}", "system\n");
292 assert_custom_format!("{metadata}", "PURPOSE_BUGFIX\n");
293
294 assert_custom_format!("name={name}|state={state}", "name=enabled_ro|state=ENABLED\n");
295 assert_custom_format!("{state}{state}{state}", "ENABLEDENABLEDENABLED\n");
296 }
297
298 #[test]
test_create_filter_predicate()299 fn test_create_filter_predicate() {
300 macro_rules! assert_create_filter_predicate {
301 ($filter:expr, $expected:expr) => {
302 let parsed_flags = parse_test_flags();
303 let predicate = create_filter_predicate($filter).unwrap();
304 let mut filtered_flags: Vec<String> = parsed_flags
305 .parsed_flag
306 .into_iter()
307 .filter(predicate)
308 .map(|flag| flag.fully_qualified_name())
309 .collect();
310 filtered_flags.sort();
311 assert_eq!(&filtered_flags, $expected);
312 };
313 }
314
315 assert_create_filter_predicate!(
316 "package:com.android.aconfig.test",
317 &[
318 "com.android.aconfig.test.disabled_ro",
319 "com.android.aconfig.test.disabled_rw",
320 "com.android.aconfig.test.disabled_rw_exported",
321 "com.android.aconfig.test.disabled_rw_in_other_namespace",
322 "com.android.aconfig.test.enabled_fixed_ro",
323 "com.android.aconfig.test.enabled_fixed_ro_exported",
324 "com.android.aconfig.test.enabled_ro",
325 "com.android.aconfig.test.enabled_ro_exported",
326 "com.android.aconfig.test.enabled_rw",
327 ]
328 );
329 assert_create_filter_predicate!(
330 "name:disabled_rw",
331 &["com.android.aconfig.test.disabled_rw"]
332 );
333 assert_create_filter_predicate!(
334 "namespace:other_namespace",
335 &["com.android.aconfig.test.disabled_rw_in_other_namespace"]
336 );
337 // description: not supported yet
338 assert_create_filter_predicate!("bug:123", &["com.android.aconfig.test.disabled_ro",]);
339 assert_create_filter_predicate!(
340 "state:ENABLED",
341 &[
342 "com.android.aconfig.test.enabled_fixed_ro",
343 "com.android.aconfig.test.enabled_fixed_ro_exported",
344 "com.android.aconfig.test.enabled_ro",
345 "com.android.aconfig.test.enabled_ro_exported",
346 "com.android.aconfig.test.enabled_rw",
347 ]
348 );
349 assert_create_filter_predicate!(
350 "permission:READ_ONLY",
351 &[
352 "com.android.aconfig.test.disabled_ro",
353 "com.android.aconfig.test.enabled_fixed_ro",
354 "com.android.aconfig.test.enabled_fixed_ro_exported",
355 "com.android.aconfig.test.enabled_ro",
356 "com.android.aconfig.test.enabled_ro_exported",
357 ]
358 );
359 // trace: not supported yet
360 assert_create_filter_predicate!(
361 "is_fixed_read_only:true",
362 &[
363 "com.android.aconfig.test.enabled_fixed_ro",
364 "com.android.aconfig.test.enabled_fixed_ro_exported",
365 ]
366 );
367 assert_create_filter_predicate!(
368 "is_exported:true",
369 &[
370 "com.android.aconfig.test.disabled_rw_exported",
371 "com.android.aconfig.test.enabled_fixed_ro_exported",
372 "com.android.aconfig.test.enabled_ro_exported",
373 ]
374 );
375 assert_create_filter_predicate!(
376 "container:system",
377 &[
378 "com.android.aconfig.test.disabled_ro",
379 "com.android.aconfig.test.disabled_rw",
380 "com.android.aconfig.test.disabled_rw_exported",
381 "com.android.aconfig.test.disabled_rw_in_other_namespace",
382 "com.android.aconfig.test.enabled_fixed_ro",
383 "com.android.aconfig.test.enabled_fixed_ro_exported",
384 "com.android.aconfig.test.enabled_ro",
385 "com.android.aconfig.test.enabled_ro_exported",
386 "com.android.aconfig.test.enabled_rw",
387 ]
388 );
389 // metadata: not supported yet
390
391 // synthesized fields
392 assert_create_filter_predicate!(
393 "fully_qualified_name:com.android.aconfig.test.disabled_rw",
394 &["com.android.aconfig.test.disabled_rw"]
395 );
396
397 // multiple sub filters
398 assert_create_filter_predicate!(
399 "permission:READ_ONLY+state:ENABLED",
400 &[
401 "com.android.aconfig.test.enabled_fixed_ro",
402 "com.android.aconfig.test.enabled_fixed_ro_exported",
403 "com.android.aconfig.test.enabled_ro",
404 "com.android.aconfig.test.enabled_ro_exported",
405 ]
406 );
407 }
408 }
409