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