1 // Copyright (C) 2024 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 //! Crate for handling Google METADATA files.
16
17 use std::{
18 fs::{read_to_string, write},
19 path::PathBuf,
20 };
21
22 use chrono::Datelike;
23 use protobuf::text_format::ParseError;
24
25 #[cfg(soong)]
26 mod metadata_proto {
27 pub use google_metadata_proto::metadata::Identifier;
28 pub use google_metadata_proto::metadata::LicenseType;
29 pub use google_metadata_proto::metadata::MetaData;
30 }
31
32 #[cfg(not(soong))]
33 mod metadata_proto {
34 include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
35 pub use crate::metadata::Identifier;
36 pub use crate::metadata::LicenseType;
37 pub use crate::metadata::MetaData;
38 }
39
40 pub use metadata_proto::*;
41
42 #[allow(missing_docs)]
43 #[derive(thiserror::Error, Debug)]
44 pub enum Error {
45 #[error("File exists: {}", .0.display())]
46 FileExists(PathBuf),
47 #[error("Crate name not set")]
48 CrateNameMissing(),
49 #[error("Crate names don't match: {} in METADATA vs {}", .0, .1)]
50 CrateNameMismatch(String, String),
51 #[error("Glob pattern error")]
52 ParseError(#[from] ParseError),
53 #[error("Write error")]
54 WriteError(#[from] std::io::Error),
55 }
56
57 /// Wrapper around a Google METADATA file.
58 pub struct GoogleMetadata {
59 path: PathBuf,
60 metadata: MetaData,
61 }
62
63 impl GoogleMetadata {
64 /// Reads an existing METADATA file.
try_from<P: Into<PathBuf>>(path: P) -> Result<Self, Error>65 pub fn try_from<P: Into<PathBuf>>(path: P) -> Result<Self, Error> {
66 let path = path.into();
67 let metadata = read_to_string(&path)?;
68 let metadata: MetaData = protobuf::text_format::parse_from_str(&metadata)?;
69 Ok(GoogleMetadata { path, metadata })
70 }
71 /// Initializes a new METADATA file.
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>72 pub fn init<P: Into<PathBuf>, Q: Into<String>, R: Into<String>, S: Into<String>>(
73 path: P,
74 name: Q,
75 version: R,
76 desc: S,
77 license_type: LicenseType,
78 ) -> Result<Self, Error> {
79 let path = path.into();
80 if path.exists() {
81 return Err(Error::FileExists(path));
82 }
83 let mut metadata = GoogleMetadata { path, metadata: MetaData::new() };
84 let name = name.into();
85 metadata.set_date_to_today()?;
86 metadata.metadata.set_name(name.clone());
87 metadata.set_version_and_urls(&name, version)?;
88 let third_party = metadata.metadata.third_party.mut_or_insert_default();
89 third_party.set_homepage(crates_io_homepage(&name));
90 third_party.set_license_type(license_type);
91 metadata.metadata.set_description(desc.into());
92 Ok(metadata)
93 }
94 /// Writes to the METADATA file.
95 ///
96 /// The existing file is overwritten.
write(&self) -> Result<(), Error>97 pub fn write(&self) -> Result<(), Error> {
98 Ok(write(&self.path, protobuf::text_format::print_to_string_pretty(&self.metadata))?)
99 }
100 /// Sets the date fields to today's date.
set_date_to_today(&mut self) -> Result<(), Error>101 pub fn set_date_to_today(&mut self) -> Result<(), Error> {
102 let now = chrono::Utc::now();
103 let date = self
104 .metadata
105 .third_party
106 .mut_or_insert_default()
107 .last_upgrade_date
108 .mut_or_insert_default();
109 date.set_day(now.day().try_into().unwrap());
110 date.set_month(now.month().try_into().unwrap());
111 date.set_year(now.year());
112 Ok(())
113 }
114 /// Sets the version and URL fields.
115 ///
116 /// Sets third_party.homepage and third_party.version, and
117 /// a single "Archive" identifier with crate archive URL and version.
set_version_and_urls<Q: Into<String>>( &mut self, name: impl AsRef<str>, version: Q, ) -> Result<(), Error>118 pub fn set_version_and_urls<Q: Into<String>>(
119 &mut self,
120 name: impl AsRef<str>,
121 version: Q,
122 ) -> Result<(), Error> {
123 let name_in_metadata = self.metadata.name.as_ref().ok_or(Error::CrateNameMissing())?;
124 if name_in_metadata != name.as_ref() {
125 return Err(Error::CrateNameMismatch(
126 name_in_metadata.clone(),
127 name.as_ref().to_string(),
128 ));
129 }
130 let third_party = self.metadata.third_party.mut_or_insert_default();
131 third_party.set_homepage(crates_io_homepage(&name));
132 let version = version.into();
133 third_party.set_version(version.clone());
134 let mut identifier = Identifier::new();
135 identifier.set_type("Archive".to_string());
136 identifier.set_value(crate_archive_url(name, &version));
137 identifier.set_version(version);
138 self.metadata.third_party.mut_or_insert_default().identifier.clear();
139 self.metadata.third_party.mut_or_insert_default().identifier.push(identifier);
140 Ok(())
141 }
142 /// Migrate homepage from an identifier to its own field.
migrate_homepage(&mut self) -> bool143 pub fn migrate_homepage(&mut self) -> bool {
144 let mut homepage = None;
145 for (idx, identifier) in self.metadata.third_party.identifier.iter().enumerate() {
146 if identifier.type_.as_ref().unwrap_or(&String::new()).to_lowercase() == "homepage" {
147 match homepage {
148 Some(info) => panic!("Homepage set twice? {info:?} {identifier:?}"),
149 None => homepage = Some((idx, identifier.clone())),
150 }
151 }
152 }
153 let Some(homepage) = homepage else { return false };
154 self.metadata.third_party.mut_or_insert_default().identifier.remove(homepage.0);
155 self.metadata.third_party.mut_or_insert_default().homepage = homepage.1.value;
156 true
157 }
158 /// Normalize case of 'Archive' identifiers.
migrate_archive(&mut self) -> bool159 pub fn migrate_archive(&mut self) -> bool {
160 let mut updated = false;
161 for identifier in self.metadata.third_party.mut_or_insert_default().identifier.iter_mut() {
162 if identifier.type_ == Some("ARCHIVE".to_string()) {
163 identifier.type_ = Some("Archive".to_string());
164 updated = true;
165 }
166 }
167 updated
168 }
169 /// Remove deprecate URL fields.
remove_deprecated_url(&mut self) -> bool170 pub fn remove_deprecated_url(&mut self) -> bool {
171 let updated = !self.metadata.third_party.url.is_empty();
172 self.metadata.third_party.mut_or_insert_default().url.clear();
173 updated
174 }
175 }
176
crate_archive_url(name: impl AsRef<str>, version: impl AsRef<str>) -> String177 fn crate_archive_url(name: impl AsRef<str>, version: impl AsRef<str>) -> String {
178 format!(
179 "https://static.crates.io/crates/{}/{}-{}.crate",
180 name.as_ref(),
181 name.as_ref(),
182 version.as_ref()
183 )
184 }
crates_io_homepage(name: impl AsRef<str>) -> String185 fn crates_io_homepage(name: impl AsRef<str>) -> String {
186 format!("https://crates.io/crates/{}", name.as_ref())
187 }
188