xref: /aosp_15_r20/external/bazelbuild-rules_rust/util/label/label.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 //! Bazel label parsing library.
2 //!
3 //! USAGE: `label::analyze("//foo/bar:baz")
4 mod label_error;
5 use label_error::LabelError;
6 
7 /// Parse and analyze given str.
8 ///
9 /// TODO: validate . and .. in target name
10 /// TODO: validate used characters in target name
analyze(input: &'_ str) -> Result<Label<'_>>11 pub fn analyze(input: &'_ str) -> Result<Label<'_>> {
12     Label::analyze(input)
13 }
14 
15 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
16 pub enum Repository<'s> {
17     /// A `@@` prefixed name that is unique within a workspace. E.g. `@@rules_rust~0.1.2~toolchains~local_rustc`
18     Canonical(&'s str), // stringifies to `@@self.0` where `self.0` may be empty
19     /// A `@` (single) prefixed name. E.g. `@rules_rust`.
20     Apparent(&'s str),
21 }
22 
23 impl<'s> Repository<'s> {
repo_name(&self) -> &'s str24     pub fn repo_name(&self) -> &'s str {
25         match self {
26             Repository::Canonical(name) => &name[2..],
27             Repository::Apparent(name) => &name[1..],
28         }
29     }
30 }
31 
32 #[derive(Debug, PartialEq, Eq)]
33 pub enum Label<'s> {
34     Relative {
35         target_name: &'s str,
36     },
37     Absolute {
38         repository: Option<Repository<'s>>,
39         package_name: &'s str,
40         target_name: &'s str,
41     },
42 }
43 
44 type Result<T, E = LabelError> = core::result::Result<T, E>;
45 
46 impl<'s> Label<'s> {
47     /// Parse and analyze given str.
analyze(input: &'s str) -> Result<Label<'s>>48     pub fn analyze(input: &'s str) -> Result<Label<'s>> {
49         let label = input;
50 
51         if label.is_empty() {
52             return Err(LabelError(err(
53                 label,
54                 "Empty string cannot be parsed into a label.",
55             )));
56         }
57 
58         if label.starts_with(':') {
59             return match consume_name(input, label)? {
60                 None => Err(LabelError(err(
61                     label,
62                     "Relative packages must have a name.",
63                 ))),
64                 Some(name) => Ok(Label::Relative { target_name: name }),
65             };
66         }
67 
68         let (input, repository) = consume_repository_name(input, label)?;
69 
70         // Shorthand labels such as `@repo` are expanded to `@repo//:repo`.
71         if input.is_empty() {
72             if let Some(ref repo) = repository {
73                 let target_name = repo.repo_name();
74                 if target_name.is_empty() {
75                     return Err(LabelError(err(
76                         label,
77                         "invalid target name: empty target name",
78                     )));
79                 } else {
80                     return Ok(Label::Absolute {
81                         repository,
82                         package_name: "",
83                         target_name,
84                     });
85                 };
86             }
87         }
88         let (input, package_name) = consume_package_name(input, label)?;
89         let name = consume_name(input, label)?;
90         let name = match (package_name, name) {
91             (None, None) => {
92                 return Err(LabelError(err(
93                     label,
94                     "labels must have a package and/or a name.",
95                 )))
96             }
97             (Some(package_name), None) => name_from_package(package_name),
98             (_, Some(name)) => name,
99         };
100 
101         Ok(Label::Absolute {
102             repository,
103             package_name: package_name.unwrap_or_default(),
104             target_name: name,
105         })
106     }
107 
is_relative(&self) -> bool108     pub fn is_relative(&self) -> bool {
109         match self {
110             Label::Absolute { .. } => false,
111             Label::Relative { .. } => true,
112         }
113     }
114 
repo(&self) -> Option<&Repository<'s>>115     pub fn repo(&self) -> Option<&Repository<'s>> {
116         match self {
117             Label::Absolute { repository, .. } => repository.as_ref(),
118             Label::Relative { .. } => None,
119         }
120     }
121 
repo_name(&self) -> Option<&'s str>122     pub fn repo_name(&self) -> Option<&'s str> {
123         match self {
124             Label::Absolute { repository, .. } => repository.as_ref().map(|repo| repo.repo_name()),
125             Label::Relative { .. } => None,
126         }
127     }
128 
package(&self) -> Option<&'s str>129     pub fn package(&self) -> Option<&'s str> {
130         match self {
131             Label::Relative { .. } => None,
132             Label::Absolute { package_name, .. } => Some(*package_name),
133         }
134     }
135 
name(&self) -> &'s str136     pub fn name(&self) -> &'s str {
137         match self {
138             Label::Relative { target_name } => target_name,
139             Label::Absolute { target_name, .. } => target_name,
140         }
141     }
142 }
143 
err<'s>(label: &'s str, msg: &'s str) -> String144 fn err<'s>(label: &'s str, msg: &'s str) -> String {
145     let mut err_msg = label.to_string();
146     err_msg.push_str(" must be a legal label; ");
147     err_msg.push_str(msg);
148     err_msg
149 }
150 
consume_repository_name<'s>( input: &'s str, label: &'s str, ) -> Result<(&'s str, Option<Repository<'s>>)>151 fn consume_repository_name<'s>(
152     input: &'s str,
153     label: &'s str,
154 ) -> Result<(&'s str, Option<Repository<'s>>)> {
155     let at_signs = {
156         let mut count = 0;
157         for char in input.chars() {
158             if char == '@' {
159                 count += 1;
160             } else {
161                 break;
162             }
163         }
164         count
165     };
166     if at_signs == 0 {
167         return Ok((input, None));
168     }
169     if at_signs > 2 {
170         return Err(LabelError(err(label, "Unexpected number of leading `@`.")));
171     }
172 
173     let slash_pos = input.find("//").unwrap_or(input.len());
174     let repository_name = &input[at_signs..slash_pos];
175 
176     if !repository_name.is_empty() {
177         if !repository_name
178             .chars()
179             .next()
180             .unwrap()
181             .is_ascii_alphabetic()
182         {
183             return Err(LabelError(err(
184                 label,
185                 "workspace names must start with a letter.",
186             )));
187         }
188         if !repository_name
189             .chars()
190             .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~')
191         {
192             return Err(LabelError(err(
193                 label,
194                 "workspace names \
195                 may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'.",
196             )));
197         }
198     }
199 
200     let repository = if at_signs == 1 {
201         Repository::Apparent(&input[0..slash_pos])
202     } else if at_signs == 2 {
203         if repository_name.is_empty() {
204             return Err(LabelError(err(
205                 label,
206                 "main repository labels are only represented by a single `@`.",
207             )));
208         }
209         Repository::Canonical(&input[0..slash_pos])
210     } else {
211         return Err(LabelError(err(label, "Unexpected number of leading `@`.")));
212     };
213 
214     Ok((&input[slash_pos..], Some(repository)))
215 }
216 
consume_package_name<'s>(input: &'s str, label: &'s str) -> Result<(&'s str, Option<&'s str>)>217 fn consume_package_name<'s>(input: &'s str, label: &'s str) -> Result<(&'s str, Option<&'s str>)> {
218     let is_absolute = match input.rfind("//") {
219         None => false,
220         Some(0) => true,
221         Some(_) => {
222             return Err(LabelError(err(
223                 label,
224                 "'//' cannot appear in the middle of the label.",
225             )));
226         }
227     };
228 
229     let (package_name, rest) = match (is_absolute, input.find(':')) {
230         (false, colon_pos) if colon_pos.map_or(true, |pos| pos != 0) => {
231             return Err(LabelError(err(
232                 label,
233                 "relative packages are not permitted.",
234             )));
235         }
236         (_, colon_pos) => {
237             let (input, colon_pos) = if is_absolute {
238                 (&input[2..], colon_pos.map(|cp| cp - 2))
239             } else {
240                 (input, colon_pos)
241             };
242             match colon_pos {
243                 Some(colon_pos) => (&input[0..colon_pos], &input[colon_pos..]),
244                 None => (input, ""),
245             }
246         }
247     };
248 
249     if package_name.is_empty() {
250         return Ok((rest, None));
251     }
252 
253     if !package_name.chars().all(|c| {
254         c.is_ascii_alphanumeric()
255             || c == '/'
256             || c == '-'
257             || c == '.'
258             || c == ' '
259             || c == '$'
260             || c == '('
261             || c == ')'
262             || c == '_'
263             || c == '+'
264     }) {
265         return Err(LabelError(err(
266             label,
267             "package names may contain only A-Z, \
268         a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')', '_', and '+'.",
269         )));
270     }
271     if package_name.ends_with('/') {
272         return Err(LabelError(err(
273             label,
274             "package names may not end with '/'.",
275         )));
276     }
277 
278     if rest.is_empty() && is_absolute {
279         // This label doesn't contain the target name, we have to use
280         // last segment of the package name as target name.
281         return Ok((
282             match package_name.rfind('/') {
283                 Some(pos) => &package_name[pos..],
284                 None => package_name,
285             },
286             Some(package_name),
287         ));
288     }
289 
290     Ok((rest, Some(package_name)))
291 }
292 
consume_name<'s>(input: &'s str, label: &'s str) -> Result<Option<&'s str>>293 fn consume_name<'s>(input: &'s str, label: &'s str) -> Result<Option<&'s str>> {
294     if input.is_empty() {
295         return Ok(None);
296     }
297     if input == ":" {
298         return Err(LabelError(err(label, "empty target name.")));
299     }
300     let name = if let Some(stripped) = input.strip_prefix(':') {
301         stripped
302     } else if let Some(stripped) = input.strip_prefix("//") {
303         stripped
304     } else {
305         input.strip_prefix('/').unwrap_or(input)
306     };
307 
308     if name.starts_with('/') {
309         return Err(LabelError(err(
310             label,
311             "target names may not start with '/'.",
312         )));
313     }
314     if name.starts_with(':') {
315         return Err(LabelError(err(
316             label,
317             "target names may not contain with ':'.",
318         )));
319     }
320     Ok(Some(name))
321 }
322 
name_from_package(package_name: &str) -> &str323 fn name_from_package(package_name: &str) -> &str {
324     package_name
325         .rsplit_once('/')
326         .map(|tup| tup.1)
327         .unwrap_or(package_name)
328 }
329 
330 #[cfg(test)]
331 mod tests {
332     use super::*;
333 
334     #[test]
test_repository_name_parsing() -> Result<()>335     fn test_repository_name_parsing() -> Result<()> {
336         assert_eq!(analyze("@repo//:foo")?.repo_name(), Some("repo"));
337         assert_eq!(analyze("@@repo//:foo")?.repo_name(), Some("repo"));
338         assert_eq!(analyze("@//:foo")?.repo_name(), Some(""));
339         assert_eq!(analyze("//:foo")?.repo_name(), None);
340         assert_eq!(analyze(":foo")?.repo_name(), None);
341 
342         assert_eq!(analyze("@repo//foo/bar")?.repo_name(), Some("repo"));
343         assert_eq!(analyze("@@repo//foo/bar")?.repo_name(), Some("repo"));
344         assert_eq!(analyze("@//foo/bar")?.repo_name(), Some(""));
345         assert_eq!(analyze("//foo/bar")?.repo_name(), None);
346         assert_eq!(
347             analyze("foo/bar"),
348             Err(LabelError(
349                 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
350             ))
351         );
352 
353         assert_eq!(analyze("@repo//foo")?.repo_name(), Some("repo"));
354         assert_eq!(analyze("@@repo//foo")?.repo_name(), Some("repo"));
355         assert_eq!(analyze("@//foo")?.repo_name(), Some(""));
356         assert_eq!(analyze("//foo")?.repo_name(), None);
357         assert_eq!(
358             analyze("foo"),
359             Err(LabelError(
360                 "foo must be a legal label; relative packages are not permitted.".to_string()
361             ))
362         );
363 
364         assert_eq!(
365             analyze("@@@repo//foo"),
366             Err(LabelError(
367                 "@@@repo//foo must be a legal label; Unexpected number of leading `@`.".to_owned()
368             ))
369         );
370 
371         assert_eq!(
372             analyze("@@@//foo:bar"),
373             Err(LabelError(
374                 "@@@//foo:bar must be a legal label; Unexpected number of leading `@`.".to_owned()
375             ))
376         );
377 
378         assert_eq!(
379             analyze("@foo:bar"),
380             Err(LabelError(
381                 "@foo:bar must be a legal label; workspace names may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'.".to_string()
382             ))
383         );
384 
385         assert_eq!(
386             analyze("@AZab0123456789_-.//:foo")?.repo_name(),
387             Some("AZab0123456789_-.")
388         );
389         assert_eq!(
390             analyze("@42//:baz"),
391             Err(LabelError(
392                 "@42//:baz must be a legal label; workspace names must \
393             start with a letter."
394                     .to_string()
395             ))
396         );
397         assert_eq!(
398             analyze("@foo#//:baz"),
399             Err(LabelError(
400                 "@foo#//:baz must be a legal label; workspace names \
401             may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'."
402                     .to_string()
403             ))
404         );
405         assert_eq!(
406             analyze("@@//foo/bar"),
407             Err(LabelError(
408                 "@@//foo/bar must be a legal label; main repository labels are only represented by a single `@`."
409                     .to_string()
410             ))
411         );
412         assert_eq!(
413             analyze("@@//:foo"),
414             Err(LabelError(
415                 "@@//:foo must be a legal label; main repository labels are only represented by a single `@`."
416                     .to_string()
417             ))
418         );
419         assert_eq!(
420             analyze("@@//foo"),
421             Err(LabelError(
422                 "@@//foo must be a legal label; main repository labels are only represented by a single `@`."
423                     .to_string()
424             ))
425         );
426 
427         assert_eq!(
428             analyze("@@"),
429             Err(LabelError(
430                 "@@ must be a legal label; main repository labels are only represented by a single `@`.".to_string()
431             )),
432         );
433 
434         Ok(())
435     }
436 
437     #[test]
test_package_name_parsing() -> Result<()>438     fn test_package_name_parsing() -> Result<()> {
439         assert_eq!(analyze("//:baz/qux")?.package(), Some(""));
440         assert_eq!(analyze(":baz/qux")?.package(), None);
441 
442         assert_eq!(analyze("//foo:baz/qux")?.package(), Some("foo"));
443         assert_eq!(analyze("//foo/bar:baz/qux")?.package(), Some("foo/bar"));
444         assert_eq!(
445             analyze("foo:baz/qux"),
446             Err(LabelError(
447                 "foo:baz/qux must be a legal label; relative packages are not permitted."
448                     .to_string()
449             ))
450         );
451         assert_eq!(
452             analyze("foo/bar:baz/qux"),
453             Err(LabelError(
454                 "foo/bar:baz/qux must be a legal label; relative packages are not permitted."
455                     .to_string()
456             ))
457         );
458 
459         assert_eq!(analyze("//foo")?.package(), Some("foo"));
460 
461         assert_eq!(
462             analyze("foo//bar"),
463             Err(LabelError(
464                 "foo//bar must be a legal label; '//' cannot appear in the middle of the label."
465                     .to_string()
466             ))
467         );
468         assert_eq!(
469             analyze("//foo//bar"),
470             Err(LabelError(
471                 "//foo//bar must be a legal label; '//' cannot appear in the middle of the label."
472                     .to_string()
473             ))
474         );
475         assert_eq!(
476             analyze("foo//bar:baz"),
477             Err(LabelError(
478                 "foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
479                     .to_string()
480             ))
481         );
482         assert_eq!(
483             analyze("//foo//bar:baz"),
484             Err(LabelError(
485                 "//foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
486                     .to_string()
487             ))
488         );
489 
490         assert_eq!(
491             analyze("//azAZ09/-. $()_:baz")?.package(),
492             Some("azAZ09/-. $()_")
493         );
494         assert_eq!(
495             analyze("//bar#:baz"),
496             Err(LabelError(
497                 "//bar#:baz must be a legal label; package names may contain only A-Z, \
498                 a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')', '_', and '+'."
499                     .to_string()
500             ))
501         );
502         assert_eq!(
503             analyze("//bar/:baz"),
504             Err(LabelError(
505                 "//bar/:baz must be a legal label; package names may not end with '/'.".to_string()
506             ))
507         );
508 
509         assert_eq!(analyze("@repo//foo/bar")?.package(), Some("foo/bar"));
510         assert_eq!(analyze("//foo/bar")?.package(), Some("foo/bar"));
511         assert_eq!(
512             analyze("foo/bar"),
513             Err(LabelError(
514                 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
515             ))
516         );
517 
518         assert_eq!(analyze("@repo//foo")?.package(), Some("foo"));
519         assert_eq!(analyze("//foo")?.package(), Some("foo"));
520         assert_eq!(
521             analyze("foo"),
522             Err(LabelError(
523                 "foo must be a legal label; relative packages are not permitted.".to_string()
524             ))
525         );
526 
527         Ok(())
528     }
529 
530     #[test]
test_name_parsing() -> Result<()>531     fn test_name_parsing() -> Result<()> {
532         assert_eq!(analyze("//foo:baz")?.name(), "baz");
533         assert_eq!(analyze("//foo:baz/qux")?.name(), "baz/qux");
534         assert_eq!(analyze(":baz/qux")?.name(), "baz/qux");
535 
536         assert_eq!(
537             analyze("::baz/qux"),
538             Err(LabelError(
539                 "::baz/qux must be a legal label; target names may not contain with ':'."
540                     .to_string()
541             ))
542         );
543 
544         assert_eq!(
545             analyze("//bar:"),
546             Err(LabelError(
547                 "//bar: must be a legal label; empty target name.".to_string()
548             ))
549         );
550         assert_eq!(analyze("//foo")?.name(), "foo");
551 
552         assert_eq!(
553             analyze("//bar:/foo"),
554             Err(LabelError(
555                 "//bar:/foo must be a legal label; target names may not start with '/'."
556                     .to_string()
557             ))
558         );
559 
560         assert_eq!(analyze("@repo//foo/bar")?.name(), "bar");
561         assert_eq!(analyze("//foo/bar")?.name(), "bar");
562         assert_eq!(
563             analyze("foo/bar"),
564             Err(LabelError(
565                 "foo/bar must be a legal label; relative packages are not permitted.".to_string()
566             ))
567         );
568 
569         assert_eq!(analyze("@repo//foo")?.name(), "foo");
570         assert_eq!(analyze("//foo")?.name(), "foo");
571         assert_eq!(
572             analyze("foo"),
573             Err(LabelError(
574                 "foo must be a legal label; relative packages are not permitted.".to_string()
575             ))
576         );
577 
578         assert_eq!(
579             analyze("@repo")?,
580             Label::Absolute {
581                 repository: Some(Repository::Apparent("@repo")),
582                 package_name: "",
583                 target_name: "repo",
584             },
585         );
586 
587         assert_eq!(
588             analyze("@"),
589             Err(LabelError(
590                 "@ must be a legal label; invalid target name: empty target name".to_string()
591             )),
592         );
593 
594         Ok(())
595     }
596 }
597