1 /*
2 * Copyright (C) 2025 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::ParsedFlagExt;
18 use anyhow::{anyhow, Context, Result};
19 use regex::Regex;
20 use std::{
21 collections::HashSet,
22 io::{BufRead, BufReader, Read},
23 };
24
25 pub(crate) type FlagId = String;
26
27 /// Grep for all flags used with @FlaggedApi annotations in an API signature file (*current.txt
28 /// file).
extract_flagged_api_flags<R: Read>(mut reader: R) -> Result<HashSet<FlagId>>29 pub(crate) fn extract_flagged_api_flags<R: Read>(mut reader: R) -> Result<HashSet<FlagId>> {
30 let mut haystack = String::new();
31 reader.read_to_string(&mut haystack)?;
32 let regex = Regex::new(r#"(?ms)@FlaggedApi\("(.*?)"\)"#).unwrap();
33 let iter = regex.captures_iter(&haystack).map(|cap| cap[1].to_owned());
34 Ok(HashSet::from_iter(iter))
35 }
36
37 /// Read a list of flag names. The input is expected to be plain text, with each line containing
38 /// the name of a single flag.
read_finalized_flags<R: Read>(reader: R) -> Result<HashSet<FlagId>>39 pub(crate) fn read_finalized_flags<R: Read>(reader: R) -> Result<HashSet<FlagId>> {
40 BufReader::new(reader)
41 .lines()
42 .map(|line_result| line_result.context("Failed to read line from finalized flags file"))
43 .collect()
44 }
45
46 /// Parse a ProtoParsedFlags binary protobuf blob and return the fully qualified names of flags
47 /// have is_exported as true.
get_exported_flags_from_binary_proto<R: Read>( mut reader: R, ) -> Result<HashSet<FlagId>>48 pub(crate) fn get_exported_flags_from_binary_proto<R: Read>(
49 mut reader: R,
50 ) -> Result<HashSet<FlagId>> {
51 let mut buffer = Vec::new();
52 reader.read_to_end(&mut buffer)?;
53 let parsed_flags = aconfig_protos::parsed_flags::try_from_binary_proto(&buffer)
54 .map_err(|_| anyhow!("failed to parse binary proto"))?;
55 let iter = parsed_flags
56 .parsed_flag
57 .into_iter()
58 .filter(|flag| flag.is_exported())
59 .map(|flag| flag.fully_qualified_name());
60 Ok(HashSet::from_iter(iter))
61 }
62
get_allow_flag_list() -> Result<HashSet<FlagId>>63 fn get_allow_flag_list() -> Result<HashSet<FlagId>> {
64 let allow_list: HashSet<FlagId> =
65 include_str!("../allow_flag_list.txt").lines().map(|x| x.into()).collect();
66 Ok(allow_list)
67 }
68
get_allow_package_list() -> Result<HashSet<FlagId>>69 fn get_allow_package_list() -> Result<HashSet<FlagId>> {
70 let allow_list: HashSet<FlagId> =
71 include_str!("../allow_package_list.txt").lines().map(|x| x.into()).collect();
72 Ok(allow_list)
73 }
74
75 /// Filter out the flags have is_exported as true but not used with @FlaggedApi annotations
76 /// in the source tree, or in the previously finalized flags set.
check_all_exported_flags( flags_used_with_flaggedapi_annotation: &HashSet<FlagId>, all_flags: &HashSet<FlagId>, already_finalized_flags: &HashSet<FlagId>, ) -> Result<Vec<FlagId>>77 pub(crate) fn check_all_exported_flags(
78 flags_used_with_flaggedapi_annotation: &HashSet<FlagId>,
79 all_flags: &HashSet<FlagId>,
80 already_finalized_flags: &HashSet<FlagId>,
81 ) -> Result<Vec<FlagId>> {
82 let allow_flag_list = get_allow_flag_list()?;
83 let allow_package_list = get_allow_package_list()?;
84
85 let new_flags: Vec<FlagId> = all_flags
86 .difference(flags_used_with_flaggedapi_annotation)
87 .cloned()
88 .collect::<HashSet<_>>()
89 .difference(already_finalized_flags)
90 .cloned()
91 .collect::<HashSet<_>>()
92 .difference(&allow_flag_list)
93 .filter(|flag| {
94 if let Some(last_dot_index) = flag.rfind('.') {
95 let package_name = &flag[..last_dot_index];
96 !allow_package_list.contains(package_name)
97 } else {
98 true
99 }
100 })
101 .cloned()
102 .collect();
103
104 Ok(new_flags)
105 }
106
107 #[cfg(test)]
108 mod tests {
109 use super::*;
110
111 #[test]
test_extract_flagged_api_flags()112 fn test_extract_flagged_api_flags() {
113 let api_signature_file = include_bytes!("../tests/api-signature-file.txt");
114 let flags = extract_flagged_api_flags(&api_signature_file[..]).unwrap();
115 assert_eq!(
116 flags,
117 HashSet::from_iter(vec![
118 "record_finalized_flags.test.foo".to_string(),
119 "this.flag.is.not.used".to_string(),
120 ])
121 );
122 }
123
124 #[test]
test_read_finalized_flags()125 fn test_read_finalized_flags() {
126 let input = include_bytes!("../tests/finalized-flags.txt");
127 let flags = read_finalized_flags(&input[..]).unwrap();
128 assert_eq!(
129 flags,
130 HashSet::from_iter(vec![
131 "record_finalized_flags.test.bar".to_string(),
132 "record_finalized_flags.test.baz".to_string(),
133 ])
134 );
135 }
136
137 #[test]
test_disabled_or_read_write_flags_are_ignored()138 fn test_disabled_or_read_write_flags_are_ignored() {
139 let bytes = include_bytes!("../tests/flags.protobuf");
140 let flags = get_exported_flags_from_binary_proto(&bytes[..]).unwrap();
141 assert_eq!(
142 flags,
143 HashSet::from_iter(vec![
144 "record_finalized_flags.test.foo".to_string(),
145 "record_finalized_flags.test.not_enabled".to_string()
146 ])
147 );
148 }
149 }
150