| /* |
| * Copyright (C) 2025 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| //! Functions to extract finalized flag information from |
| //! /prebuilts/sdk/#/finalized-flags.txt. |
| //! These functions are very specific to that file setup as well as the format |
| //! of the files (just a list of the fully-qualified flag names). |
| //! There are also some helper functions for local building using cargo. These |
| //! functions are only invoked via cargo for quick local testing and will not |
| //! be used during actual soong building. They are marked as such. |
| use anyhow::{anyhow, Result}; |
| use serde::{Deserialize, Serialize}; |
| use std::collections::{HashMap, HashSet}; |
| use std::fs; |
| use std::io::{self, BufRead}; |
| |
| const SDK_INT_MULTIPLIER: u32 = 100_000; |
| |
| /// Just the fully qualified flag name (package_name.flag_name). |
| #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] |
| pub struct FinalizedFlag { |
| /// Name of the flag. |
| pub flag_name: String, |
| /// Name of the package. |
| pub package_name: String, |
| } |
| |
| /// API level in which the flag was finalized. |
| #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] |
| pub struct ApiLevel(pub i32); |
| |
| /// API level of the extended flags file of version 35 |
| pub const EXTENDED_FLAGS_35_APILEVEL: ApiLevel = ApiLevel(35); |
| |
| /// Contains all flags finalized for a given API level. |
| #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)] |
| pub struct FinalizedFlagMap(HashMap<ApiLevel, HashSet<FinalizedFlag>>); |
| |
| impl FinalizedFlagMap { |
| /// Creates a new, empty instance. |
| pub fn new() -> Self { |
| Self(HashMap::new()) |
| } |
| |
| /// Convenience method for is_empty on the underlying map. |
| pub fn is_empty(&self) -> bool { |
| self.0.is_empty() |
| } |
| |
| /// Returns the API level in which the flag was finalized . |
| pub fn get_finalized_level(&self, flag: &FinalizedFlag) -> Option<ApiLevel> { |
| for (api_level, flags_for_level) in &self.0 { |
| if flags_for_level.contains(flag) { |
| return Some(*api_level); |
| } |
| } |
| None |
| } |
| |
| /// Insert the flag into the map for the given level if the flag is not |
| /// present in the map already - for *any* level (not just the one given). |
| pub fn insert_if_new(&mut self, level: ApiLevel, flag: FinalizedFlag) { |
| if self.contains(&flag) { |
| return; |
| } |
| self.0.entry(level).or_default().insert(flag); |
| } |
| |
| fn contains(&self, flag: &FinalizedFlag) -> bool { |
| self.0.values().any(|flags_set| flags_set.contains(flag)) |
| } |
| } |
| |
| #[allow(dead_code)] // TODO: b/378936061: Use with SDK_INT_FULL check. |
| fn parse_full_version(version: String) -> Result<u32> { |
| let (major, minor) = if let Some(decimal_index) = version.find('.') { |
| (version[..decimal_index].parse::<u32>()?, version[decimal_index + 1..].parse::<u32>()?) |
| } else { |
| (version.parse::<u32>()?, 0) |
| }; |
| |
| if major >= 21474 { |
| return Err(anyhow!("Major version too large, must be less than 21474.")); |
| } |
| if minor >= SDK_INT_MULTIPLIER { |
| return Err(anyhow!("Minor version too large, must be less than {}.", SDK_INT_MULTIPLIER)); |
| } |
| |
| Ok(major * SDK_INT_MULTIPLIER + minor) |
| } |
| |
| const EXTENDED_FLAGS_LIST_35: &str = "extended_flags_list_35.txt"; |
| |
| /// Converts a string to an int. Will parse to int even if the string is "X.0". |
| /// Returns error for "X.1". |
| fn str_to_api_level(numeric_string: &str) -> Result<ApiLevel> { |
| let float_value = numeric_string.parse::<f64>()?; |
| |
| if float_value.fract() == 0.0 { |
| Ok(ApiLevel(float_value as i32)) |
| } else { |
| Err(anyhow!("Numeric string is float, can't parse to int.")) |
| } |
| } |
| |
| /// For each file, extracts the qualified flag names into a FinalizedFlag, then |
| /// enters them in a map at the API level corresponding to their directory. |
| /// Ex: /prebuilts/sdk/35/finalized-flags.txt -> {36, [flag1, flag2]}. |
| pub fn read_files_to_map_using_path(flag_files: Vec<String>) -> Result<FinalizedFlagMap> { |
| let mut data_map = FinalizedFlagMap::new(); |
| |
| for flag_file in flag_files { |
| // Split /path/sdk/<int.int>/finalized-flags.txt -> ['/path/sdk', 'int.int', 'finalized-flags.txt']. |
| let flag_file_split: Vec<String> = |
| flag_file.clone().rsplitn(3, '/').map(|s| s.to_string()).collect(); |
| |
| if &flag_file_split[0] != "finalized-flags.txt" { |
| return Err(anyhow!("Provided incorrect file, must be finalized-flags.txt")); |
| } |
| |
| let api_level_string = &flag_file_split[1]; |
| |
| // For now, skip any directory with full API level, e.g. "36.1". The |
| // finalized flag files each contain all flags finalized *up to* that |
| // level (including prior levels), so skipping intermediate levels means |
| // the flags will be included at the next full number. |
| // TODO: b/378936061 - Support full SDK version. |
| // In the future, we should error if provided a non-numeric directory. |
| let Ok(api_level) = str_to_api_level(api_level_string) else { |
| continue; |
| }; |
| |
| let file = fs::File::open(&flag_file)?; |
| |
| io::BufReader::new(file).lines().for_each(|flag| { |
| let flag = |
| flag.unwrap_or_else(|_| panic!("Failed to read line from file {}", flag_file)); |
| let finalized_flag = build_finalized_flag(&flag) |
| .unwrap_or_else(|_| panic!("cannot build finalized flag {}", flag)); |
| data_map.insert_if_new(api_level, finalized_flag); |
| }); |
| } |
| |
| Ok(data_map) |
| } |
| |
| /// Read the qualified flag names into a FinalizedFlag set |
| pub fn read_extend_file_to_map_using_path(extened_file: String) -> Result<HashSet<FinalizedFlag>> { |
| let (_, file_name) = |
| extened_file.rsplit_once('/').ok_or(anyhow!("Invalid file: '{}'", extened_file))?; |
| if file_name != EXTENDED_FLAGS_LIST_35 { |
| return Err(anyhow!("Provided incorrect file, must be {}", EXTENDED_FLAGS_LIST_35)); |
| } |
| let file = fs::File::open(extened_file)?; |
| let extended_flags = io::BufReader::new(file) |
| .lines() |
| .map(|flag| { |
| let flag = flag.expect("Failed to read line from extended file"); |
| build_finalized_flag(&flag) |
| .unwrap_or_else(|_| panic!("cannot build finalized flag {}", flag)) |
| }) |
| .collect::<HashSet<FinalizedFlag>>(); |
| Ok(extended_flags) |
| } |
| |
| fn build_finalized_flag(qualified_flag_name: &String) -> Result<FinalizedFlag> { |
| // Split the qualified flag name into package and flag name: |
| // com.my.package.name.my_flag_name -> ('com.my.package.name', 'my_flag_name') |
| let (package_name, flag_name) = qualified_flag_name |
| .rsplit_once('.') |
| .ok_or(anyhow!("Invalid qualified flag name format: '{}'", qualified_flag_name))?; |
| |
| Ok(FinalizedFlag { flag_name: flag_name.to_string(), package_name: package_name.to_string() }) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::fs::File; |
| use std::io::Write; |
| use tempfile::tempdir; |
| |
| const FLAG_FILE_NAME: &str = "finalized-flags.txt"; |
| |
| // Creates some flags for testing. |
| fn create_test_flags() -> Vec<FinalizedFlag> { |
| vec![ |
| FinalizedFlag { flag_name: "name1".to_string(), package_name: "package1".to_string() }, |
| FinalizedFlag { flag_name: "name2".to_string(), package_name: "package2".to_string() }, |
| FinalizedFlag { flag_name: "name3".to_string(), package_name: "package3".to_string() }, |
| ] |
| } |
| |
| // Writes the fully qualified flag names in the given file. |
| fn add_flags_to_file(flag_file: &mut File, flags: &[FinalizedFlag]) { |
| for flag in flags { |
| let _unused = writeln!(flag_file, "{}.{}", flag.package_name, flag.flag_name); |
| } |
| } |
| |
| #[test] |
| fn test_read_flags_one_file() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/finalized-flags.txt. |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path = temp_dir.path().to_path_buf(); |
| file_path.push("35"); |
| fs::create_dir_all(&file_path).unwrap(); |
| file_path.push(FLAG_FILE_NAME); |
| let mut file = File::create(&file_path).unwrap(); |
| |
| // Write all flags to the file. |
| add_flags_to_file(&mut file, &[flags[0].clone(), flags[1].clone()]); |
| let flag_file_path = file_path.to_string_lossy().to_string(); |
| |
| // Convert to map. |
| let map = read_files_to_map_using_path(vec![flag_file_path]).unwrap(); |
| |
| assert_eq!(map.0.len(), 1); |
| assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0])); |
| assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[1])); |
| } |
| |
| #[test] |
| fn test_read_flags_two_files() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/finalized-flags.txt and for 36. |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path1 = temp_dir.path().to_path_buf(); |
| file_path1.push("35"); |
| fs::create_dir_all(&file_path1).unwrap(); |
| file_path1.push(FLAG_FILE_NAME); |
| let mut file1 = File::create(&file_path1).unwrap(); |
| |
| let mut file_path2 = temp_dir.path().to_path_buf(); |
| file_path2.push("36"); |
| fs::create_dir_all(&file_path2).unwrap(); |
| file_path2.push(FLAG_FILE_NAME); |
| let mut file2 = File::create(&file_path2).unwrap(); |
| |
| // Write all flags to the files. |
| add_flags_to_file(&mut file1, &[flags[0].clone()]); |
| add_flags_to_file(&mut file2, &[flags[0].clone(), flags[1].clone(), flags[2].clone()]); |
| let flag_file_path1 = file_path1.to_string_lossy().to_string(); |
| let flag_file_path2 = file_path2.to_string_lossy().to_string(); |
| |
| // Convert to map. |
| let map = read_files_to_map_using_path(vec![flag_file_path1, flag_file_path2]).unwrap(); |
| |
| // Assert there are two API levels, 35 and 36. |
| assert_eq!(map.0.len(), 2); |
| assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0])); |
| |
| // 36 should not have the first flag in the set, as it was finalized in |
| // an earlier API level. |
| assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[1])); |
| assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[2])); |
| } |
| |
| #[test] |
| fn test_read_flags_full_numbers() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/finalized-flags.txt and for 36. |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path1 = temp_dir.path().to_path_buf(); |
| file_path1.push("35.0"); |
| fs::create_dir_all(&file_path1).unwrap(); |
| file_path1.push(FLAG_FILE_NAME); |
| let mut file1 = File::create(&file_path1).unwrap(); |
| |
| let mut file_path2 = temp_dir.path().to_path_buf(); |
| file_path2.push("36.0"); |
| fs::create_dir_all(&file_path2).unwrap(); |
| file_path2.push(FLAG_FILE_NAME); |
| let mut file2 = File::create(&file_path2).unwrap(); |
| |
| // Write all flags to the files. |
| add_flags_to_file(&mut file1, &[flags[0].clone()]); |
| add_flags_to_file(&mut file2, &[flags[0].clone(), flags[1].clone(), flags[2].clone()]); |
| let flag_file_path1 = file_path1.to_string_lossy().to_string(); |
| let flag_file_path2 = file_path2.to_string_lossy().to_string(); |
| |
| // Convert to map. |
| let map = read_files_to_map_using_path(vec![flag_file_path1, flag_file_path2]).unwrap(); |
| |
| assert_eq!(map.0.len(), 2); |
| assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0])); |
| assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[1])); |
| assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[2])); |
| } |
| |
| #[test] |
| fn test_read_flags_fractions_round_up() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/finalized-flags.txt and for 36. |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path1 = temp_dir.path().to_path_buf(); |
| file_path1.push("35.1"); |
| fs::create_dir_all(&file_path1).unwrap(); |
| file_path1.push(FLAG_FILE_NAME); |
| let mut file1 = File::create(&file_path1).unwrap(); |
| |
| let mut file_path2 = temp_dir.path().to_path_buf(); |
| file_path2.push("36.0"); |
| fs::create_dir_all(&file_path2).unwrap(); |
| file_path2.push(FLAG_FILE_NAME); |
| let mut file2 = File::create(&file_path2).unwrap(); |
| |
| // Write all flags to the files. |
| add_flags_to_file(&mut file1, &[flags[0].clone()]); |
| add_flags_to_file(&mut file2, &[flags[0].clone(), flags[1].clone(), flags[2].clone()]); |
| let flag_file_path1 = file_path1.to_string_lossy().to_string(); |
| let flag_file_path2 = file_path2.to_string_lossy().to_string(); |
| |
| // Convert to map. |
| let map = read_files_to_map_using_path(vec![flag_file_path1, flag_file_path2]).unwrap(); |
| |
| // No flags were added in 35. All 35.1 flags were rolled up to 36. |
| assert_eq!(map.0.len(), 1); |
| assert!(!map.0.contains_key(&ApiLevel(35))); |
| assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[0])); |
| assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[1])); |
| assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[2])); |
| } |
| |
| #[test] |
| fn test_read_flags_non_numeric() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/finalized-flags.txt. |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path = temp_dir.path().to_path_buf(); |
| file_path.push("35"); |
| fs::create_dir_all(&file_path).unwrap(); |
| file_path.push(FLAG_FILE_NAME); |
| let mut flag_file = File::create(&file_path).unwrap(); |
| |
| let mut invalid_path = temp_dir.path().to_path_buf(); |
| invalid_path.push("sdk-annotations"); |
| fs::create_dir_all(&invalid_path).unwrap(); |
| invalid_path.push(FLAG_FILE_NAME); |
| File::create(&invalid_path).unwrap(); |
| |
| // Write all flags to the file. |
| add_flags_to_file(&mut flag_file, &[flags[0].clone(), flags[1].clone()]); |
| let flag_file_path = file_path.to_string_lossy().to_string(); |
| |
| // Convert to map. |
| let map = read_files_to_map_using_path(vec![ |
| flag_file_path, |
| invalid_path.to_string_lossy().to_string(), |
| ]) |
| .unwrap(); |
| |
| // No set should be created for sdk-annotations. |
| assert_eq!(map.0.len(), 1); |
| assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0])); |
| assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[1])); |
| } |
| |
| #[test] |
| fn test_read_flags_wrong_file_err() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/finalized-flags.txt. |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path = temp_dir.path().to_path_buf(); |
| file_path.push("35"); |
| fs::create_dir_all(&file_path).unwrap(); |
| file_path.push(FLAG_FILE_NAME); |
| let mut flag_file = File::create(&file_path).unwrap(); |
| |
| let mut pre_flag_path = temp_dir.path().to_path_buf(); |
| pre_flag_path.push("18"); |
| fs::create_dir_all(&pre_flag_path).unwrap(); |
| pre_flag_path.push("some_random_file.txt"); |
| File::create(&pre_flag_path).unwrap(); |
| |
| // Write all flags to the file. |
| add_flags_to_file(&mut flag_file, &[flags[0].clone(), flags[1].clone()]); |
| let flag_file_path = file_path.to_string_lossy().to_string(); |
| |
| // Convert to map. |
| let map = read_files_to_map_using_path(vec![ |
| flag_file_path, |
| pre_flag_path.to_string_lossy().to_string(), |
| ]); |
| |
| assert!(map.is_err()); |
| } |
| |
| #[test] |
| fn test_flags_map_insert_if_new() { |
| let flags = create_test_flags(); |
| let mut map = FinalizedFlagMap::new(); |
| let l35 = ApiLevel(35); |
| let l36 = ApiLevel(36); |
| |
| map.insert_if_new(l35, flags[0].clone()); |
| map.insert_if_new(l35, flags[1].clone()); |
| map.insert_if_new(l35, flags[2].clone()); |
| map.insert_if_new(l36, flags[0].clone()); |
| |
| assert!(map.0.get(&l35).unwrap().contains(&flags[0])); |
| assert!(map.0.get(&l35).unwrap().contains(&flags[1])); |
| assert!(map.0.get(&l35).unwrap().contains(&flags[2])); |
| assert!(!map.0.contains_key(&l36)); |
| } |
| |
| #[test] |
| fn test_flags_map_get_level() { |
| let flags = create_test_flags(); |
| let mut map = FinalizedFlagMap::new(); |
| let l35 = ApiLevel(35); |
| let l36 = ApiLevel(36); |
| |
| map.insert_if_new(l35, flags[0].clone()); |
| map.insert_if_new(l36, flags[1].clone()); |
| |
| assert_eq!(map.get_finalized_level(&flags[0]).unwrap(), l35); |
| assert_eq!(map.get_finalized_level(&flags[1]).unwrap(), l36); |
| } |
| |
| #[test] |
| fn test_read_flag_from_extended_file() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/extended_flags_list_35.txt |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path = temp_dir.path().to_path_buf(); |
| file_path.push("35"); |
| fs::create_dir_all(&file_path).unwrap(); |
| file_path.push(EXTENDED_FLAGS_LIST_35); |
| let mut file = File::create(&file_path).unwrap(); |
| |
| // Write all flags to the file. |
| add_flags_to_file(&mut file, &[flags[0].clone(), flags[1].clone()]); |
| |
| let flags_set = |
| read_extend_file_to_map_using_path(file_path.to_string_lossy().to_string()).unwrap(); |
| assert_eq!(flags_set.len(), 2); |
| assert!(flags_set.contains(&flags[0])); |
| assert!(flags_set.contains(&flags[1])); |
| } |
| |
| #[test] |
| fn test_read_flag_from_wrong_extended_file_err() { |
| let flags = create_test_flags(); |
| |
| // Create the file <temp_dir>/35/extended_flags_list.txt |
| let temp_dir = tempdir().unwrap(); |
| let mut file_path = temp_dir.path().to_path_buf(); |
| file_path.push("35"); |
| fs::create_dir_all(&file_path).unwrap(); |
| file_path.push("extended_flags_list.txt"); |
| let mut file = File::create(&file_path).unwrap(); |
| |
| // Write all flags to the file. |
| add_flags_to_file(&mut file, &[flags[0].clone(), flags[1].clone()]); |
| |
| let err = read_extend_file_to_map_using_path(file_path.to_string_lossy().to_string()) |
| .unwrap_err(); |
| assert_eq!( |
| format!("{:?}", err), |
| "Provided incorrect file, must be extended_flags_list_35.txt" |
| ); |
| } |
| |
| #[test] |
| fn test_parse_full_version_correct_input_major_dot_minor() { |
| let version = parse_full_version("12.34".to_string()); |
| |
| assert!(version.is_ok()); |
| assert_eq!(version.unwrap(), 1_200_034); |
| } |
| |
| #[test] |
| fn test_parse_full_version_correct_input_omit_dot_minor() { |
| let version = parse_full_version("1234".to_string()); |
| |
| assert!(version.is_ok()); |
| assert_eq!(version.unwrap(), 123_400_000); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_empty_string() { |
| let version = parse_full_version("".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_no_numbers_in_string() { |
| let version = parse_full_version("hello".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_unexpected_patch_version() { |
| let version = parse_full_version("1.2.3".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_leading_dot_missing_major_version() { |
| let version = parse_full_version(".1234".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_trailing_dot_missing_minor_version() { |
| let version = parse_full_version("1234.".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_negative_major_version() { |
| let version = parse_full_version("-12.34".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_negative_minor_version() { |
| let version = parse_full_version("12.-34".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_major_version_too_large() { |
| let version = parse_full_version("40000.1".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| |
| #[test] |
| fn test_parse_full_version_incorrect_input_minor_version_too_large() { |
| let version = parse_full_version("3.99999999".to_string()); |
| |
| assert!(version.is_err()); |
| } |
| } |