record-finalized-flags: add actual implementation

From the command's --help text:

---- 8< ----
The prebuilts/sdk/<version>/finalized-flags.txt files list all aconfig flags that have been used
with @FlaggedApi annotations on APIs that have been finalized. These files are used to prevent
flags from being re-used for new, unfinalized, APIs, and by the aconfig code generation.

This tool works as follows:

  - Read API signature files from source tree (*current.txt files) [--api-signature-file]
  - Read the current aconfig flag values from source tree [--flag-file]
  - Read the previous finalized-flags.txt files from prebuilts/sdk [--finalized-flag-file]
  - Extract the flags slated for API finalization by scanning through the API signature files for
    flags that are ENABLED and READ_ONLY
  - Merge the found flags with the recorded flags from previos API finalizations
  - Print the set of flags to stdout
---- >8 ----

Bug: 377676163
Test: atest record-finalized-flags-test
Merged-In: Icde8c63fc54791429865168989bfb6af01845d15
Change-Id: Icde8c63fc54791429865168989bfb6af01845d15
diff --git a/tools/record-finalized-flags/Android.bp b/tools/record-finalized-flags/Android.bp
index 45065e4..55a3a38 100644
--- a/tools/record-finalized-flags/Android.bp
+++ b/tools/record-finalized-flags/Android.bp
@@ -9,7 +9,10 @@
     lints: "android",
     srcs: ["src/main.rs"],
     rustlibs: [
+        "libaconfig_protos",
         "libanyhow",
+        "libclap",
+        "libregex",
     ],
 }
 
diff --git a/tools/record-finalized-flags/Cargo.toml b/tools/record-finalized-flags/Cargo.toml
index ae895a4..0fc7953 100644
--- a/tools/record-finalized-flags/Cargo.toml
+++ b/tools/record-finalized-flags/Cargo.toml
@@ -9,4 +9,7 @@
 edition = "2021"
 
 [dependencies]
+aconfig_protos = { path = "../aconfig/aconfig_protos" }
 anyhow = { path = "../../../../external/rust/android-crates-io/crates/anyhow" }
+clap = { path = "../../../../external/rust/android-crates-io/crates/clap", features = ["derive"] }
+regex = { path = "../../../../external/rust/android-crates-io/crates/regex" }
diff --git a/tools/record-finalized-flags/src/api_signature_files.rs b/tools/record-finalized-flags/src/api_signature_files.rs
new file mode 100644
index 0000000..af8f4d1
--- /dev/null
+++ b/tools/record-finalized-flags/src/api_signature_files.rs
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+use anyhow::Result;
+use regex::Regex;
+use std::{collections::HashSet, io::Read};
+
+use crate::FlagId;
+
+/// Grep for all flags used with @FlaggedApi annotations in an API signature file (*current.txt
+/// file).
+pub(crate) fn extract_flagged_api_flags<R: Read>(mut reader: R) -> Result<HashSet<FlagId>> {
+    let mut haystack = String::new();
+    reader.read_to_string(&mut haystack)?;
+    let regex = Regex::new(r#"(?ms)@FlaggedApi\("(.*?)"\)"#).unwrap();
+    let iter = regex.captures_iter(&haystack).map(|cap| cap[1].to_owned());
+    Ok(HashSet::from_iter(iter))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test() {
+        let api_signature_file = include_bytes!("../tests/api-signature-file.txt");
+        let flags = extract_flagged_api_flags(&api_signature_file[..]).unwrap();
+        assert_eq!(
+            flags,
+            HashSet::from_iter(vec![
+                "record_finalized_flags.test.foo".to_string(),
+                "this.flag.is.not.used".to_string(),
+            ])
+        );
+    }
+}
diff --git a/tools/record-finalized-flags/src/finalized_flags.rs b/tools/record-finalized-flags/src/finalized_flags.rs
new file mode 100644
index 0000000..1ae4c4d
--- /dev/null
+++ b/tools/record-finalized-flags/src/finalized_flags.rs
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+use anyhow::Result;
+use std::{collections::HashSet, io::Read};
+
+use crate::FlagId;
+
+/// Read a list of flag names. The input is expected to be plain text, with each line containing
+/// the name of a single flag.
+pub(crate) fn read_finalized_flags<R: Read>(mut reader: R) -> Result<HashSet<FlagId>> {
+    let mut contents = String::new();
+    reader.read_to_string(&mut contents)?;
+    let iter = contents.lines().map(|s| s.to_owned());
+    Ok(HashSet::from_iter(iter))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test() {
+        let input = include_bytes!("../tests/finalized-flags.txt");
+        let flags = read_finalized_flags(&input[..]).unwrap();
+        assert_eq!(
+            flags,
+            HashSet::from_iter(vec![
+                "record_finalized_flags.test.bar".to_string(),
+                "record_finalized_flags.test.baz".to_string(),
+            ])
+        );
+    }
+}
diff --git a/tools/record-finalized-flags/src/flag_values.rs b/tools/record-finalized-flags/src/flag_values.rs
new file mode 100644
index 0000000..cc16d12
--- /dev/null
+++ b/tools/record-finalized-flags/src/flag_values.rs
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+use aconfig_protos::{ParsedFlagExt, ProtoFlagPermission, ProtoFlagState};
+use anyhow::{anyhow, Result};
+use std::{collections::HashSet, io::Read};
+
+use crate::FlagId;
+
+/// Parse a ProtoParsedFlags binary protobuf blob and return the fully qualified names of flags
+/// that are slated for API finalization (i.e. are both ENABLED and READ_ONLY).
+pub(crate) fn get_relevant_flags_from_binary_proto<R: Read>(
+    mut reader: R,
+) -> Result<HashSet<FlagId>> {
+    let mut buffer = Vec::new();
+    reader.read_to_end(&mut buffer)?;
+    let parsed_flags = aconfig_protos::parsed_flags::try_from_binary_proto(&buffer)
+        .map_err(|_| anyhow!("failed to parse binary proto"))?;
+    let iter = parsed_flags
+        .parsed_flag
+        .into_iter()
+        .filter(|flag| {
+            flag.state() == ProtoFlagState::ENABLED
+                && flag.permission() == ProtoFlagPermission::READ_ONLY
+        })
+        .map(|flag| flag.fully_qualified_name());
+    Ok(HashSet::from_iter(iter))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_disabled_or_read_write_flags_are_ignored() {
+        let bytes = include_bytes!("../tests/flags.protobuf");
+        let flags = get_relevant_flags_from_binary_proto(&bytes[..]).unwrap();
+        assert_eq!(flags, HashSet::from_iter(vec!["record_finalized_flags.test.foo".to_string()]));
+    }
+}
diff --git a/tools/record-finalized-flags/src/main.rs b/tools/record-finalized-flags/src/main.rs
index ceb975e..efdbc9b 100644
--- a/tools/record-finalized-flags/src/main.rs
+++ b/tools/record-finalized-flags/src/main.rs
@@ -17,14 +17,118 @@
 //! `record-finalized-flags` is a tool to create a snapshot (intended to be stored in
 //! prebuilts/sdk) of the flags used with @FlaggedApi APIs
 use anyhow::Result;
+use clap::Parser;
+use std::{collections::HashSet, fs::File, path::PathBuf};
+
+mod api_signature_files;
+mod finalized_flags;
+mod flag_values;
+
+pub(crate) type FlagId = String;
+
+const ABOUT: &str = "Create a new prebuilts/sdk/<version>/finalized-flags.txt file
+
+The prebuilts/sdk/<version>/finalized-flags.txt files list all aconfig flags that have been used
+with @FlaggedApi annotations on APIs that have been finalized. These files are used to prevent
+flags from being re-used for new, unfinalized, APIs, and by the aconfig code generation.
+
+This tool works as follows:
+
+  - Read API signature files from source tree (*current.txt files) [--api-signature-file]
+  - Read the current aconfig flag values from source tree [--parsed-flags-file]
+  - Read the previous finalized-flags.txt files from prebuilts/sdk [--finalized-flags-file]
+  - Extract the flags slated for API finalization by scanning through the API signature files for
+    flags that are ENABLED and READ_ONLY
+  - Merge the found flags with the recorded flags from previous API finalizations
+  - Print the set of flags to stdout
+";
+
+#[derive(Parser, Debug)]
+#[clap(about=ABOUT)]
+struct Cli {
+    #[arg(long)]
+    parsed_flags_file: PathBuf,
+
+    #[arg(long)]
+    api_signature_file: Vec<PathBuf>,
+
+    #[arg(long)]
+    finalized_flags_file: PathBuf,
+}
+
+/// Filter out the ENABLED and READ_ONLY flags used with @FlaggedApi annotations in the source
+/// tree, and add those flags to the set of previously finalized flags.
+fn calculate_new_finalized_flags(
+    flags_used_with_flaggedapi_annotation: &HashSet<FlagId>,
+    all_flags_to_be_finalized: &HashSet<FlagId>,
+    already_finalized_flags: &HashSet<FlagId>,
+) -> HashSet<FlagId> {
+    let new_flags: HashSet<_> = flags_used_with_flaggedapi_annotation
+        .intersection(all_flags_to_be_finalized)
+        .map(|s| s.to_owned())
+        .collect();
+    already_finalized_flags.union(&new_flags).map(|s| s.to_owned()).collect()
+}
 
 fn main() -> Result<()> {
-    println!("{:?}", std::env::args());
+    let args = Cli::parse();
+
+    let mut flags_used_with_flaggedapi_annotation = HashSet::new();
+    for path in args.api_signature_file {
+        let file = File::open(path)?;
+        for flag in api_signature_files::extract_flagged_api_flags(file)?.drain() {
+            flags_used_with_flaggedapi_annotation.insert(flag);
+        }
+    }
+
+    let file = File::open(args.parsed_flags_file)?;
+    let all_flags_to_be_finalized = flag_values::get_relevant_flags_from_binary_proto(file)?;
+
+    let file = File::open(args.finalized_flags_file)?;
+    let already_finalized_flags = finalized_flags::read_finalized_flags(file)?;
+
+    let mut new_finalized_flags = Vec::from_iter(calculate_new_finalized_flags(
+        &flags_used_with_flaggedapi_annotation,
+        &all_flags_to_be_finalized,
+        &already_finalized_flags,
+    ));
+    new_finalized_flags.sort();
+
+    println!("{}", new_finalized_flags.join("\n"));
+
     Ok(())
 }
 
 #[cfg(test)]
 mod tests {
+    use super::*;
+
     #[test]
-    fn test() {}
+    fn test() {
+        let input = include_bytes!("../tests/api-signature-file.txt");
+        let flags_used_with_flaggedapi_annotation =
+            api_signature_files::extract_flagged_api_flags(&input[..]).unwrap();
+
+        let input = include_bytes!("../tests/flags.protobuf");
+        let all_flags_to_be_finalized =
+            flag_values::get_relevant_flags_from_binary_proto(&input[..]).unwrap();
+
+        let input = include_bytes!("../tests/finalized-flags.txt");
+        let already_finalized_flags = finalized_flags::read_finalized_flags(&input[..]).unwrap();
+
+        let new_finalized_flags = calculate_new_finalized_flags(
+            &flags_used_with_flaggedapi_annotation,
+            &all_flags_to_be_finalized,
+            &already_finalized_flags,
+        );
+
+        assert_eq!(
+            new_finalized_flags,
+            HashSet::from_iter(vec![
+                "record_finalized_flags.test.foo".to_string(),
+                "record_finalized_flags.test.bar".to_string(),
+                "record_finalized_flags.test.baz".to_string(),
+            ])
+        );
+    }
 }
diff --git a/tools/record-finalized-flags/tests/api-signature-file.txt b/tools/record-finalized-flags/tests/api-signature-file.txt
new file mode 100644
index 0000000..2ad559f
--- /dev/null
+++ b/tools/record-finalized-flags/tests/api-signature-file.txt
@@ -0,0 +1,15 @@
+// Signature format: 2.0
+package android {
+
+  public final class C {
+    ctor public C();
+  }
+
+  public static final class C.inner {
+    ctor public C.inner();
+    field @FlaggedApi("record_finalized_flags.test.foo") public static final String FOO = "foo";
+    field @FlaggedApi("this.flag.is.not.used") public static final String BAR = "bar";
+  }
+
+}
+
diff --git a/tools/record-finalized-flags/tests/finalized-flags.txt b/tools/record-finalized-flags/tests/finalized-flags.txt
new file mode 100644
index 0000000..7fbcb3d
--- /dev/null
+++ b/tools/record-finalized-flags/tests/finalized-flags.txt
@@ -0,0 +1,2 @@
+record_finalized_flags.test.bar
+record_finalized_flags.test.baz
diff --git a/tools/record-finalized-flags/tests/flags.declarations b/tools/record-finalized-flags/tests/flags.declarations
new file mode 100644
index 0000000..b45ef62
--- /dev/null
+++ b/tools/record-finalized-flags/tests/flags.declarations
@@ -0,0 +1,16 @@
+package: "record_finalized_flags.test"
+container: "system"
+
+flag {
+    name: "foo"
+    namespace: "test"
+    description: "FIXME"
+    bug: ""
+}
+
+flag {
+    name: "not_enabled"
+    namespace: "test"
+    description: "FIXME"
+    bug: ""
+}
diff --git a/tools/record-finalized-flags/tests/flags.protobuf b/tools/record-finalized-flags/tests/flags.protobuf
new file mode 100644
index 0000000..7c6e63e
--- /dev/null
+++ b/tools/record-finalized-flags/tests/flags.protobuf
Binary files differ
diff --git a/tools/record-finalized-flags/tests/flags.values b/tools/record-finalized-flags/tests/flags.values
new file mode 100644
index 0000000..ff6225d
--- /dev/null
+++ b/tools/record-finalized-flags/tests/flags.values
@@ -0,0 +1,13 @@
+flag_value {
+    package: "record_finalized_flags.test"
+    name: "foo"
+    state: ENABLED
+    permission: READ_ONLY
+}
+
+flag_value {
+    package: "record_finalized_flags.test"
+    name: "not_enabled"
+    state: DISABLED
+    permission: READ_ONLY
+}
diff --git a/tools/record-finalized-flags/tests/generate-flags-protobuf.sh b/tools/record-finalized-flags/tests/generate-flags-protobuf.sh
new file mode 100755
index 0000000..701189c
--- /dev/null
+++ b/tools/record-finalized-flags/tests/generate-flags-protobuf.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+aconfig create-cache \
+    --package record_finalized_flags.test \
+    --container system \
+    --declarations flags.declarations \
+    --values flags.values \
+    --cache flags.protobuf