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