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