xref: /aosp_15_r20/external/bazelbuild-rules_rust/crate_universe/src/utils/starlark/label.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 use std::fmt::{self, Display};
2 use std::path::Path;
3 use std::str::FromStr;
4 
5 use anyhow::{anyhow, bail, Context, Result};
6 use camino::Utf8Path;
7 use once_cell::sync::OnceCell;
8 use regex::Regex;
9 use serde::de::Visitor;
10 use serde::{Deserialize, Serialize, Serializer};
11 
12 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
13 pub(crate) enum Label {
14     Relative {
15         target: String,
16     },
17     Absolute {
18         repository: Repository,
19         package: String,
20         target: String,
21     },
22 }
23 
24 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
25 pub(crate) enum Repository {
26     Canonical(String), // stringifies to `@@self.0` where `self.0` may be empty
27     Explicit(String),  // stringifies to `@self.0` where `self.0` may be empty
28     Local,             // stringifies to the empty string
29 }
30 
31 impl Label {
32     #[cfg(test)]
is_absolute(&self) -> bool33     pub(crate) fn is_absolute(&self) -> bool {
34         match self {
35             Label::Relative { .. } => false,
36             Label::Absolute { .. } => true,
37         }
38     }
39 
40     #[cfg(test)]
repository(&self) -> Option<&Repository>41     pub(crate) fn repository(&self) -> Option<&Repository> {
42         match self {
43             Label::Relative { .. } => None,
44             Label::Absolute { repository, .. } => Some(repository),
45         }
46     }
47 
package(&self) -> Option<&str>48     pub(crate) fn package(&self) -> Option<&str> {
49         match self {
50             Label::Relative { .. } => None,
51             Label::Absolute { package, .. } => Some(package.as_str()),
52         }
53     }
54 
target(&self) -> &str55     pub(crate) fn target(&self) -> &str {
56         match self {
57             Label::Relative { target } => target.as_str(),
58             Label::Absolute { target, .. } => target.as_str(),
59         }
60     }
61 }
62 
63 impl FromStr for Label {
64     type Err = anyhow::Error;
65 
from_str(s: &str) -> Result<Self, Self::Err>66     fn from_str(s: &str) -> Result<Self, Self::Err> {
67         static RE: OnceCell<Regex> = OnceCell::new();
68         let re = RE.get_or_try_init(|| {
69             Regex::new(r"^(@@?[\w\d\-_\.~]*)?(//)?([\w\d\-_\./+]+)?(:([\+\w\d\-_\./]+))?$")
70         });
71 
72         let cap = re?
73             .captures(s)
74             .with_context(|| format!("Failed to parse label from string: {s}"))?;
75 
76         let (repository, is_absolute) = match (cap.get(1), cap.get(2).is_some()) {
77             (Some(repository), is_absolute) => match *repository.as_str().as_bytes() {
78                 [b'@', b'@', ..] => (
79                     Some(Repository::Canonical(repository.as_str()[2..].to_owned())),
80                     is_absolute,
81                 ),
82                 [b'@', ..] => (
83                     Some(Repository::Explicit(repository.as_str()[1..].to_owned())),
84                     is_absolute,
85                 ),
86                 _ => bail!("Invalid Label: {}", s),
87             },
88             (None, true) => (Some(Repository::Local), true),
89             (None, false) => (None, false),
90         };
91 
92         let package = cap.get(3).map(|package| package.as_str().to_owned());
93 
94         let target = cap.get(5).map(|target| target.as_str().to_owned());
95 
96         match repository {
97             None => match (package, target) {
98                 // Relative
99                 (None, Some(target)) => Ok(Label::Relative { target }),
100 
101                 // Relative (Implicit Target which regex identifies as Package)
102                 (Some(package), None) => Ok(Label::Relative { target: package }),
103 
104                 // Invalid (Empty)
105                 (None, None) => bail!("Invalid Label: {}", s),
106 
107                 // Invalid (Relative Package + Target)
108                 (Some(_), Some(_)) => bail!("Invalid Label: {}", s),
109             },
110             Some(repository) => match (is_absolute, package, target) {
111                 // Absolute (Full)
112                 (true, Some(package), Some(target)) => Ok(Label::Absolute {
113                     repository,
114                     package,
115                     target,
116                 }),
117 
118                 // Absolute (Repository)
119                 (_, None, None) => match &repository {
120                     Repository::Canonical(target) | Repository::Explicit(target) => {
121                         let target = match target.is_empty() {
122                             false => target.clone(),
123                             true => bail!("Invalid Label: {}", s),
124                         };
125                         Ok(Label::Absolute {
126                             repository,
127                             package: String::new(),
128                             target,
129                         })
130                     }
131                     Repository::Local => bail!("Invalid Label: {}", s),
132                 },
133 
134                 // Absolute (Package)
135                 (true, Some(package), None) => {
136                     let target = Utf8Path::new(&package)
137                         .file_name()
138                         .with_context(|| format!("Invalid Label: {}", s))?
139                         .to_owned();
140                     Ok(Label::Absolute {
141                         repository,
142                         package,
143                         target,
144                     })
145                 }
146 
147                 // Absolute (Target)
148                 (true, None, Some(target)) => Ok(Label::Absolute {
149                     repository,
150                     package: String::new(),
151                     target,
152                 }),
153 
154                 // Invalid (Relative Repository + Package + Target)
155                 (false, Some(_), Some(_)) => bail!("Invalid Label: {}", s),
156 
157                 // Invalid (Relative Repository + Package)
158                 (false, Some(_), None) => bail!("Invalid Label: {}", s),
159 
160                 // Invalid (Relative Repository + Target)
161                 (false, None, Some(_)) => bail!("Invalid Label: {}", s),
162             },
163         }
164     }
165 }
166 
167 impl Display for Label {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result168     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169         match self {
170             Label::Relative { target } => write!(f, ":{}", target),
171             Label::Absolute {
172                 repository,
173                 package,
174                 target,
175             } => match repository {
176                 Repository::Canonical(repository) => {
177                     write!(f, "@@{repository}//{package}:{target}")
178                 }
179                 Repository::Explicit(repository) => {
180                     write!(f, "@{repository}//{package}:{target}")
181                 }
182                 Repository::Local => write!(f, "//{package}:{target}"),
183             },
184         }
185     }
186 }
187 
188 impl Label {
189     /// Generates a label appropriate for the passed Path by walking the filesystem to identify its
190     /// workspace and package.
from_absolute_path(p: &Path) -> Result<Self, anyhow::Error>191     pub(crate) fn from_absolute_path(p: &Path) -> Result<Self, anyhow::Error> {
192         let mut workspace_root = None;
193         let mut package_root = None;
194         for ancestor in p.ancestors().skip(1) {
195             if package_root.is_none()
196                 && (ancestor.join("BUILD").exists() || ancestor.join("BUILD.bazel").exists())
197             {
198                 package_root = Some(ancestor);
199             }
200             if workspace_root.is_none()
201                 && (ancestor.join("WORKSPACE").exists()
202                     || ancestor.join("WORKSPACE.bazel").exists()
203                     || ancestor.join("MODULE.bazel").exists())
204             {
205                 workspace_root = Some(ancestor);
206                 break;
207             }
208         }
209         match (workspace_root, package_root) {
210             (Some(workspace_root), Some(package_root)) => {
211                 // These unwraps are safe by construction of the ancestors and prefix calls which set up these paths.
212                 let target = p.strip_prefix(package_root).unwrap();
213                 let workspace_relative = p.strip_prefix(workspace_root).unwrap();
214                 let mut package_path = workspace_relative.to_path_buf();
215                 for _ in target.components() {
216                     package_path.pop();
217                 }
218 
219                 let package = if package_path.components().count() > 0 {
220                     path_to_label_part(&package_path)?
221                 } else {
222                     String::new()
223                 };
224                 let target = path_to_label_part(target)?;
225 
226                 Ok(Label::Absolute {
227                     repository: Repository::Local,
228                     package,
229                     target,
230                 })
231             }
232             (Some(_workspace_root), None) => {
233                 bail!(
234                     "Could not identify package for path {}. Maybe you need to add a BUILD.bazel file.",
235                     p.display()
236                 );
237             }
238             _ => {
239                 bail!("Could not identify workspace for path {}", p.display());
240             }
241         }
242     }
243 }
244 
245 /// Converts a path to a forward-slash-delimited label-appropriate path string.
path_to_label_part(path: &Path) -> Result<String, anyhow::Error>246 fn path_to_label_part(path: &Path) -> Result<String, anyhow::Error> {
247     let components: Result<Vec<_>, _> = path
248         .components()
249         .map(|c| {
250             c.as_os_str().to_str().ok_or_else(|| {
251                 anyhow!(
252                     "Found non-UTF8 component turning path into label: {}",
253                     path.display()
254                 )
255             })
256         })
257         .collect();
258     Ok(components?.join("/"))
259 }
260 
261 impl Serialize for Label {
serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer,262     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
263     where
264         S: Serializer,
265     {
266         serializer.serialize_str(&self.repr())
267     }
268 }
269 
270 struct LabelVisitor;
271 impl<'de> Visitor<'de> for LabelVisitor {
272     type Value = Label;
273 
expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result274     fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
275         formatter.write_str("Expected string value of `{name} {version}`.")
276     }
277 
visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: serde::de::Error,278     fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
279     where
280         E: serde::de::Error,
281     {
282         Label::from_str(v).map_err(E::custom)
283     }
284 }
285 
286 impl<'de> Deserialize<'de> for Label {
deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>,287     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288     where
289         D: serde::Deserializer<'de>,
290     {
291         deserializer.deserialize_str(LabelVisitor)
292     }
293 }
294 
295 impl Label {
repr(&self) -> String296     pub(crate) fn repr(&self) -> String {
297         self.to_string()
298     }
299 }
300 
301 #[cfg(test)]
302 mod test {
303     use super::*;
304     use spectral::prelude::*;
305     use std::fs::{create_dir_all, File};
306     use tempfile::tempdir;
307 
308     #[test]
relative()309     fn relative() {
310         let label = Label::from_str(":target").unwrap();
311         assert_eq!(label.to_string(), ":target");
312         assert!(!label.is_absolute());
313         assert_eq!(label.repository(), None);
314         assert_eq!(label.package(), None);
315         assert_eq!(label.target(), "target");
316     }
317 
318     #[test]
relative_implicit()319     fn relative_implicit() {
320         let label = Label::from_str("target").unwrap();
321         assert_eq!(label.to_string(), ":target");
322         assert!(!label.is_absolute());
323         assert_eq!(label.repository(), None);
324         assert_eq!(label.package(), None);
325         assert_eq!(label.target(), "target");
326     }
327 
328     #[test]
absolute_full()329     fn absolute_full() {
330         let label = Label::from_str("@repo//package:target").unwrap();
331         assert_eq!(label.to_string(), "@repo//package:target");
332         assert!(label.is_absolute());
333         assert_eq!(
334             label.repository(),
335             Some(&Repository::Explicit(String::from("repo")))
336         );
337         assert_eq!(label.package(), Some("package"));
338         assert_eq!(label.target(), "target");
339     }
340 
341     #[test]
absolute_repository()342     fn absolute_repository() {
343         let label = Label::from_str("@repo").unwrap();
344         assert_eq!(label.to_string(), "@repo//:repo");
345         assert!(label.is_absolute());
346         assert_eq!(
347             label.repository(),
348             Some(&Repository::Explicit(String::from("repo")))
349         );
350         assert_eq!(label.package(), Some(""));
351         assert_eq!(label.target(), "repo");
352     }
353 
354     #[test]
absolute_package()355     fn absolute_package() {
356         let label = Label::from_str("//package").unwrap();
357         assert_eq!(label.to_string(), "//package:package");
358         assert!(label.is_absolute());
359         assert_eq!(label.repository(), Some(&Repository::Local));
360         assert_eq!(label.package(), Some("package"));
361         assert_eq!(label.target(), "package");
362 
363         let label = Label::from_str("//package/subpackage").unwrap();
364         assert_eq!(label.to_string(), "//package/subpackage:subpackage");
365         assert!(label.is_absolute());
366         assert_eq!(label.repository(), Some(&Repository::Local));
367         assert_eq!(label.package(), Some("package/subpackage"));
368         assert_eq!(label.target(), "subpackage");
369     }
370 
371     #[test]
absolute_target()372     fn absolute_target() {
373         let label = Label::from_str("//:target").unwrap();
374         assert_eq!(label.to_string(), "//:target");
375         assert!(label.is_absolute());
376         assert_eq!(label.repository(), Some(&Repository::Local));
377         assert_eq!(label.package(), Some(""));
378         assert_eq!(label.target(), "target");
379     }
380 
381     #[test]
absolute_repository_package()382     fn absolute_repository_package() {
383         let label = Label::from_str("@repo//package").unwrap();
384         assert_eq!(label.to_string(), "@repo//package:package");
385         assert!(label.is_absolute());
386         assert_eq!(
387             label.repository(),
388             Some(&Repository::Explicit(String::from("repo")))
389         );
390         assert_eq!(label.package(), Some("package"));
391         assert_eq!(label.target(), "package");
392     }
393 
394     #[test]
absolute_repository_target()395     fn absolute_repository_target() {
396         let label = Label::from_str("@repo//:target").unwrap();
397         assert_eq!(label.to_string(), "@repo//:target");
398         assert!(label.is_absolute());
399         assert_eq!(
400             label.repository(),
401             Some(&Repository::Explicit(String::from("repo")))
402         );
403         assert_eq!(label.package(), Some(""));
404         assert_eq!(label.target(), "target");
405     }
406 
407     #[test]
absolute_package_target()408     fn absolute_package_target() {
409         let label = Label::from_str("//package:target").unwrap();
410         assert_eq!(label.to_string(), "//package:target");
411         assert!(label.is_absolute());
412         assert_eq!(label.repository(), Some(&Repository::Local));
413         assert_eq!(label.package(), Some("package"));
414         assert_eq!(label.target(), "target");
415     }
416 
417     #[test]
invalid_empty()418     fn invalid_empty() {
419         Label::from_str("").unwrap_err();
420         Label::from_str("@").unwrap_err();
421         Label::from_str("//").unwrap_err();
422         Label::from_str(":").unwrap_err();
423     }
424 
425     #[test]
invalid_relative_repository_package_target()426     fn invalid_relative_repository_package_target() {
427         Label::from_str("@repo/package:target").unwrap_err();
428     }
429 
430     #[test]
invalid_relative_repository_package()431     fn invalid_relative_repository_package() {
432         Label::from_str("@repo/package").unwrap_err();
433     }
434 
435     #[test]
invalid_relative_repository_target()436     fn invalid_relative_repository_target() {
437         Label::from_str("@repo:target").unwrap_err();
438     }
439 
440     #[test]
invalid_relative_package_target()441     fn invalid_relative_package_target() {
442         Label::from_str("package:target").unwrap_err();
443     }
444 
445     #[test]
full_label_bzlmod()446     fn full_label_bzlmod() {
447         let label = Label::from_str("@@repo//package/sub_package:target").unwrap();
448         assert_eq!(label.to_string(), "@@repo//package/sub_package:target");
449         assert!(label.is_absolute());
450         assert_eq!(
451             label.repository(),
452             Some(&Repository::Canonical(String::from("repo")))
453         );
454         assert_eq!(label.package(), Some("package/sub_package"));
455         assert_eq!(label.target(), "target");
456     }
457 
458     #[test]
full_label_bzlmod_with_tilde()459     fn full_label_bzlmod_with_tilde() {
460         let label = Label::from_str("@@repo~name//package/sub_package:target").unwrap();
461         assert_eq!(label.to_string(), "@@repo~name//package/sub_package:target");
462         assert!(label.is_absolute());
463         assert_eq!(
464             label.repository(),
465             Some(&Repository::Canonical(String::from("repo~name")))
466         );
467         assert_eq!(label.package(), Some("package/sub_package"));
468         assert_eq!(label.target(), "target");
469     }
470 
471     #[test]
full_label_with_slash_after_colon()472     fn full_label_with_slash_after_colon() {
473         let label = Label::from_str("@repo//package/sub_package:subdir/target").unwrap();
474         assert_eq!(
475             label.to_string(),
476             "@repo//package/sub_package:subdir/target"
477         );
478         assert!(label.is_absolute());
479         assert_eq!(
480             label.repository(),
481             Some(&Repository::Explicit(String::from("repo")))
482         );
483         assert_eq!(label.package(), Some("package/sub_package"));
484         assert_eq!(label.target(), "subdir/target");
485     }
486 
487     #[test]
label_contains_plus()488     fn label_contains_plus() {
489         let label = Label::from_str("@repo//vendor/wasi-0.11.0+wasi-snapshot-preview1:BUILD.bazel")
490             .unwrap();
491         assert!(label.is_absolute());
492         assert_eq!(
493             label.repository(),
494             Some(&Repository::Explicit(String::from("repo")))
495         );
496         assert_eq!(
497             label.package(),
498             Some("vendor/wasi-0.11.0+wasi-snapshot-preview1")
499         );
500         assert_eq!(label.target(), "BUILD.bazel");
501     }
502 
503     #[test]
invalid_double_colon()504     fn invalid_double_colon() {
505         Label::from_str("::target").unwrap_err();
506     }
507 
508     #[test]
invalid_triple_at()509     fn invalid_triple_at() {
510         Label::from_str("@@@repo//pkg:target").unwrap_err();
511     }
512 
513     #[test]
from_absolute_path_exists()514     fn from_absolute_path_exists() {
515         let dir = tempdir().unwrap();
516         let workspace = dir.path().join("WORKSPACE.bazel");
517         let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
518         let subdir = dir.path().join("parent").join("child").join("grandchild");
519         let actual_file = subdir.join("greatgrandchild");
520         create_dir_all(subdir).unwrap();
521         {
522             File::create(workspace).unwrap();
523             File::create(build_file).unwrap();
524             File::create(&actual_file).unwrap();
525         }
526         let label = Label::from_absolute_path(&actual_file).unwrap();
527         assert_eq!(
528             label.to_string(),
529             "//parent/child:grandchild/greatgrandchild"
530         );
531         assert!(label.is_absolute());
532         assert_eq!(label.repository(), Some(&Repository::Local));
533         assert_eq!(label.package(), Some("parent/child"));
534         assert_eq!(label.target(), "grandchild/greatgrandchild");
535     }
536 
537     #[test]
from_absolute_path_no_workspace()538     fn from_absolute_path_no_workspace() {
539         let dir = tempdir().unwrap();
540         let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
541         let subdir = dir.path().join("parent").join("child").join("grandchild");
542         let actual_file = subdir.join("greatgrandchild");
543         create_dir_all(subdir).unwrap();
544         {
545             File::create(build_file).unwrap();
546             File::create(&actual_file).unwrap();
547         }
548         let err = Label::from_absolute_path(&actual_file)
549             .unwrap_err()
550             .to_string();
551         assert_that(&err).contains("Could not identify workspace");
552         assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
553     }
554 
555     #[test]
from_absolute_path_no_build_file()556     fn from_absolute_path_no_build_file() {
557         let dir = tempdir().unwrap();
558         let workspace = dir.path().join("WORKSPACE.bazel");
559         let subdir = dir.path().join("parent").join("child").join("grandchild");
560         let actual_file = subdir.join("greatgrandchild");
561         create_dir_all(subdir).unwrap();
562         {
563             File::create(workspace).unwrap();
564             File::create(&actual_file).unwrap();
565         }
566         let err = Label::from_absolute_path(&actual_file)
567             .unwrap_err()
568             .to_string();
569         assert_that(&err).contains("Could not identify package");
570         assert_that(&err).contains("Maybe you need to add a BUILD.bazel file");
571         assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
572     }
573 }
574