Move GoogleMetadata to a separate crate.

Bug: 339424309
Test: treehugger
Change-Id: Iad394080afc7ad7885b2104acc26692f9cc0dbee
diff --git a/tools/external_crates/Cargo.toml b/tools/external_crates/Cargo.toml
index ab31806..cfcb6d7 100644
--- a/tools/external_crates/Cargo.toml
+++ b/tools/external_crates/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
     "crate_health",
+    "google_metadata",
     "license_checker",
     "name_and_version",
     "name_and_version_proc_macros",
diff --git a/tools/external_crates/crate_health/Cargo.toml b/tools/external_crates/crate_health/Cargo.toml
index 2894dc9..fb955e8 100644
--- a/tools/external_crates/crate_health/Cargo.toml
+++ b/tools/external_crates/crate_health/Cargo.toml
@@ -25,9 +25,10 @@
 tinytemplate = "1.2"
 walkdir = "2"
 whoami = "1"
+google_metadata = { path = "../google_metadata"}
+license_checker = { path = "../license_checker" }
 name_and_version = { path = "../name_and_version" }
 name_and_version_proc_macros = { path = "../name_and_version_proc_macros" }
-license_checker = { path = "../license_checker" }
 rooted_path = { path = "../rooted_path" }
 
 [build-dependencies]
diff --git a/tools/external_crates/crate_health/src/google_metadata.rs b/tools/external_crates/crate_health/src/google_metadata.rs
deleted file mode 100644
index 17b1a7a..0000000
--- a/tools/external_crates/crate_health/src/google_metadata.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (C) 2024 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 std::{
-    fs::{read_to_string, write},
-    path::PathBuf,
-};
-
-use anyhow::{anyhow, Result};
-use chrono::{DateTime, Datelike, Local};
-use license_checker::LicenseState;
-use name_and_version::NamedAndVersioned;
-
-use crate::{
-    license::most_restrictive_type,
-    metadata::{self, Identifier, MetaData},
-};
-
-pub struct GoogleMetadata {
-    path: PathBuf,
-    metadata: MetaData,
-}
-
-impl GoogleMetadata {
-    pub fn try_from<P: Into<PathBuf>>(path: P) -> Result<Self> {
-        let path = path.into();
-        let metadata = read_to_string(&path)?;
-        let metadata: metadata::MetaData = protobuf::text_format::parse_from_str(&metadata)?;
-        Ok(GoogleMetadata { path, metadata })
-    }
-    pub fn init<P: Into<PathBuf>>(
-        path: P,
-        nv: &dyn NamedAndVersioned,
-        desc: &str,
-        licenses: &LicenseState,
-    ) -> Result<Self> {
-        let path = path.into();
-        if path.exists() {
-            return Err(anyhow!("{} already exists", path.display()));
-        }
-        let mut metadata = GoogleMetadata { path, metadata: MetaData::new() };
-        metadata.metadata.set_name(nv.name().to_string());
-        metadata.metadata.set_description(desc.to_string());
-        metadata.set_date_to_today()?;
-        metadata.set_identifier(nv)?;
-        let third_party = metadata.metadata.third_party.mut_or_insert_default();
-        third_party.set_homepage(nv.crates_io_homepage());
-        third_party.set_license_type(most_restrictive_type(licenses));
-        Ok(metadata)
-    }
-    pub fn write(&self) -> Result<()> {
-        Ok(write(&self.path, protobuf::text_format::print_to_string_pretty(&self.metadata))?)
-    }
-    pub fn set_date_to_today(&mut self) -> Result<()> {
-        let now: DateTime<Local> = Local::now();
-        let date = self
-            .metadata
-            .third_party
-            .mut_or_insert_default()
-            .last_upgrade_date
-            .mut_or_insert_default();
-        date.set_day(now.day().try_into()?);
-        date.set_month(now.month().try_into()?);
-        date.set_year(now.year());
-        Ok(())
-    }
-    pub fn set_identifier(&mut self, nv: &dyn NamedAndVersioned) -> Result<()> {
-        if self.metadata.name.as_ref().ok_or(anyhow!("No name"))? != nv.name() {
-            return Err(anyhow!("Names don't match"));
-        }
-        let mut identifier = Identifier::new();
-        identifier.set_type("Archive".to_string());
-        identifier.set_value(nv.crate_archive_url());
-        identifier.set_version(nv.version().to_string());
-        self.metadata.third_party.mut_or_insert_default().identifier.clear();
-        self.metadata.third_party.mut_or_insert_default().identifier.push(identifier);
-        Ok(())
-    }
-    pub fn migrate_homepage(&mut self) -> bool {
-        let mut homepage = None;
-        for (idx, identifier) in self.metadata.third_party.identifier.iter().enumerate() {
-            if identifier.type_.as_ref().unwrap_or(&String::new()).to_lowercase() == "homepage" {
-                match homepage {
-                    Some(info) => panic!("Homepage set twice? {info:?} {identifier:?}"),
-                    None => homepage = Some((idx, identifier.clone())),
-                }
-            }
-        }
-        let Some(homepage) = homepage else { return false };
-        self.metadata.third_party.mut_or_insert_default().identifier.remove(homepage.0);
-        self.metadata.third_party.mut_or_insert_default().homepage = homepage.1.value;
-        true
-    }
-    pub fn migrate_archive(&mut self) -> bool {
-        let mut updated = false;
-        for identifier in self.metadata.third_party.mut_or_insert_default().identifier.iter_mut() {
-            if identifier.type_ == Some("ARCHIVE".to_string()) {
-                identifier.type_ = Some("Archive".to_string());
-                updated = true;
-            }
-        }
-        updated
-    }
-}
diff --git a/tools/external_crates/crate_health/src/lib.rs b/tools/external_crates/crate_health/src/lib.rs
index cae566a..e0bba78 100644
--- a/tools/external_crates/crate_health/src/lib.rs
+++ b/tools/external_crates/crate_health/src/lib.rs
@@ -41,9 +41,6 @@
 };
 mod android_bp;
 
-pub use self::google_metadata::GoogleMetadata;
-mod google_metadata;
-
 pub use self::license::{most_restrictive_type, update_module_license_files};
 mod license;
 
@@ -111,5 +108,3 @@
         .run_quiet_and_expect_success()?;
     Ok(())
 }
-
-include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
diff --git a/tools/external_crates/crate_health/src/license.rs b/tools/external_crates/crate_health/src/license.rs
index 6ec75d6..32a81b7 100644
--- a/tools/external_crates/crate_health/src/license.rs
+++ b/tools/external_crates/crate_health/src/license.rs
@@ -18,9 +18,9 @@
     path::Path,
 };
 
-use crate::metadata::LicenseType;
 use anyhow::{anyhow, Result};
 use glob::glob;
+use google_metadata::metadata::LicenseType;
 use lazy_static::lazy_static;
 use license_checker::LicenseState;
 use spdx::{LicenseReq, Licensee};
diff --git a/tools/external_crates/crate_health/src/managed_repo.rs b/tools/external_crates/crate_health/src/managed_repo.rs
index c117429..f809a47 100644
--- a/tools/external_crates/crate_health/src/managed_repo.rs
+++ b/tools/external_crates/crate_health/src/managed_repo.rs
@@ -23,6 +23,7 @@
 
 use anyhow::{anyhow, Result};
 use glob::glob;
+use google_metadata::GoogleMetadata;
 use itertools::Itertools;
 use license_checker::find_licenses;
 use name_and_version::{NameAndVersion, NameAndVersionMap, NameAndVersionRef, NamedAndVersioned};
@@ -31,8 +32,8 @@
 use spdx::Licensee;
 
 use crate::{
-    cargo_embargo_autoconfig, copy_dir, update_module_license_files, Crate, CrateCollection,
-    GoogleMetadata, Migratable, PseudoCrate, VersionMatch,
+    cargo_embargo_autoconfig, copy_dir, most_restrictive_type, update_module_license_files, Crate,
+    CrateCollection, Migratable, PseudoCrate, VersionMatch,
 };
 
 pub struct ManagedRepo {
@@ -421,10 +422,11 @@
             update_module_license_files(&krate.path().abs(), &licenses)?;
 
             let metadata = GoogleMetadata::init(
-                krate.path().abs().join("METADATA"),
-                &krate,
+                krate.path().join("METADATA")?,
+                krate.name(),
+                krate.version().to_string(),
                 krate.description(),
-                &licenses,
+                most_restrictive_type(&licenses),
             )?;
             metadata.write()?;
 
diff --git a/tools/external_crates/crate_health/src/version_match.rs b/tools/external_crates/crate_health/src/version_match.rs
index a1985e6..37dc342 100644
--- a/tools/external_crates/crate_health/src/version_match.rs
+++ b/tools/external_crates/crate_health/src/version_match.rs
@@ -15,9 +15,10 @@
 use std::{collections::BTreeMap, path::Path};
 
 use anyhow::{anyhow, Result};
+use google_metadata::GoogleMetadata;
 use name_and_version::{NameAndVersion, NameAndVersionMap, NamedAndVersioned};
 
-use crate::{generate_android_bps, CrateCollection, GoogleMetadata, Migratable};
+use crate::{generate_android_bps, CrateCollection, Migratable};
 
 #[derive(Debug)]
 pub struct VersionPair<'a, T> {
@@ -203,7 +204,7 @@
             writeback |= metadata.migrate_archive();
             if pair.source.version() != pair.dest.version() {
                 metadata.set_date_to_today()?;
-                metadata.set_identifier(pair.dest)?;
+                metadata.set_identifier(pair.dest.name(), pair.dest.version().to_string())?;
                 writeback |= true;
             }
             if writeback {
diff --git a/tools/external_crates/google_metadata/.gitignore b/tools/external_crates/google_metadata/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/tools/external_crates/google_metadata/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/tools/external_crates/google_metadata/Cargo.toml b/tools/external_crates/google_metadata/Cargo.toml
new file mode 100644
index 0000000..5130c10
--- /dev/null
+++ b/tools/external_crates/google_metadata/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "google_metadata"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+chrono = "0.4"
+protobuf = "3"
+thiserror = "1.0"
+
+[build-dependencies]
+protobuf-codegen = "3"
+protobuf-parse = "3"
\ No newline at end of file
diff --git a/tools/external_crates/crate_health/build.rs b/tools/external_crates/google_metadata/build.rs
similarity index 100%
rename from tools/external_crates/crate_health/build.rs
rename to tools/external_crates/google_metadata/build.rs
diff --git a/tools/external_crates/google_metadata/src/lib.rs b/tools/external_crates/google_metadata/src/lib.rs
new file mode 100644
index 0000000..fba892b
--- /dev/null
+++ b/tools/external_crates/google_metadata/src/lib.rs
@@ -0,0 +1,159 @@
+// Copyright (C) 2024 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 std::{
+    fs::{read_to_string, write},
+    path::PathBuf,
+};
+
+use chrono::{DateTime, Datelike, Local};
+use protobuf::text_format::ParseError;
+
+include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
+use crate::metadata::{Identifier, LicenseType, MetaData};
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error("File exists: {}", .0.display())]
+    FileExists(PathBuf),
+    #[error("Crate name not set")]
+    CrateNameMissing(),
+    #[error("Crate names don't match: {} in METADATA vs {}", .0, .1)]
+    CrateNameMismatch(String, String),
+    #[error("Glob pattern error")]
+    ParseError(#[from] ParseError),
+    #[error("Write error")]
+    WriteError(#[from] std::io::Error),
+}
+
+pub struct GoogleMetadata {
+    path: PathBuf,
+    metadata: MetaData,
+}
+
+impl GoogleMetadata {
+    /// Reads an existing METADATA file.
+    pub fn try_from<P: Into<PathBuf>>(path: P) -> Result<Self, Error> {
+        let path = path.into();
+        let metadata = read_to_string(&path)?;
+        let metadata: metadata::MetaData = protobuf::text_format::parse_from_str(&metadata)?;
+        Ok(GoogleMetadata { path, metadata })
+    }
+    /// Initializes a new METADATA file.
+    pub fn init<P: Into<PathBuf>, Q: Into<String>, R: Into<String>, S: Into<String>>(
+        path: P,
+        name: Q,
+        version: R,
+        desc: S,
+        license_type: LicenseType,
+    ) -> Result<Self, Error> {
+        let path = path.into();
+        if path.exists() {
+            return Err(Error::FileExists(path));
+        }
+        let mut metadata = GoogleMetadata { path, metadata: MetaData::new() };
+        let name = name.into();
+        metadata.set_date_to_today()?;
+        metadata.set_identifier(&name, version)?;
+        let third_party = metadata.metadata.third_party.mut_or_insert_default();
+        third_party.set_homepage(crates_io_homepage(&name));
+        third_party.set_license_type(license_type);
+        metadata.metadata.set_name(name);
+        metadata.metadata.set_description(desc.into());
+        Ok(metadata)
+    }
+    /// Writes to the METADATA file.
+    ///
+    /// The existing file is overwritten.
+    pub fn write(&self) -> Result<(), Error> {
+        Ok(write(&self.path, protobuf::text_format::print_to_string_pretty(&self.metadata))?)
+    }
+    /// Sets the date fields to today's date.
+    pub fn set_date_to_today(&mut self) -> Result<(), Error> {
+        let now: DateTime<Local> = Local::now();
+        let date = self
+            .metadata
+            .third_party
+            .mut_or_insert_default()
+            .last_upgrade_date
+            .mut_or_insert_default();
+        date.set_day(now.day().try_into().unwrap());
+        date.set_month(now.month().try_into().unwrap());
+        date.set_year(now.year());
+        Ok(())
+    }
+    /// Sets the identifier.
+    ///
+    /// The identifier contains the URL and version for the crate.
+    pub fn set_identifier<Q: Into<String>>(
+        &mut self,
+        name: impl AsRef<str>,
+        version: Q,
+    ) -> Result<(), Error> {
+        let name_in_metadata = self.metadata.name.as_ref().ok_or(Error::CrateNameMissing())?;
+        if name_in_metadata != name.as_ref() {
+            return Err(Error::CrateNameMismatch(
+                name_in_metadata.clone(),
+                name.as_ref().to_string(),
+            ));
+        }
+        let mut identifier = Identifier::new();
+        identifier.set_type("Archive".to_string());
+        let version = version.into();
+        identifier.set_value(crate_archive_url(name, &version));
+        identifier.set_version(version);
+        self.metadata.third_party.mut_or_insert_default().identifier.clear();
+        self.metadata.third_party.mut_or_insert_default().identifier.push(identifier);
+        Ok(())
+    }
+    /// Migrate homepage from an identifier to its own field.
+    pub fn migrate_homepage(&mut self) -> bool {
+        let mut homepage = None;
+        for (idx, identifier) in self.metadata.third_party.identifier.iter().enumerate() {
+            if identifier.type_.as_ref().unwrap_or(&String::new()).to_lowercase() == "homepage" {
+                match homepage {
+                    Some(info) => panic!("Homepage set twice? {info:?} {identifier:?}"),
+                    None => homepage = Some((idx, identifier.clone())),
+                }
+            }
+        }
+        let Some(homepage) = homepage else { return false };
+        self.metadata.third_party.mut_or_insert_default().identifier.remove(homepage.0);
+        self.metadata.third_party.mut_or_insert_default().homepage = homepage.1.value;
+        true
+    }
+    /// Normalize case of 'Archive' identifiers.
+    pub fn migrate_archive(&mut self) -> bool {
+        let mut updated = false;
+        for identifier in self.metadata.third_party.mut_or_insert_default().identifier.iter_mut() {
+            if identifier.type_ == Some("ARCHIVE".to_string()) {
+                identifier.type_ = Some("Archive".to_string());
+                updated = true;
+            }
+        }
+        updated
+    }
+}
+
+fn crate_archive_url(name: impl AsRef<str>, version: impl AsRef<str>) -> String {
+    format!(
+        "https://static.crates.io/crates/{}/{}-{}.crate",
+        name.as_ref(),
+        name.as_ref(),
+        version.as_ref()
+    )
+}
+fn crates_io_homepage(name: impl AsRef<str>) -> String {
+    format!("https://crates.io/crates/{}", name.as_ref())
+}
diff --git a/tools/external_crates/crate_health/src/protos/metadata.proto b/tools/external_crates/google_metadata/src/protos/metadata.proto
similarity index 100%
rename from tools/external_crates/crate_health/src/protos/metadata.proto
rename to tools/external_crates/google_metadata/src/protos/metadata.proto