Squashed 'third_party/rules_rust/' content from commit bf59038ca
git-subtree-dir: third_party/rules_rust
git-subtree-split: bf59038cac11798cbaef9f3bf965bad8182b97fa
Signed-off-by: Brian Silverman <bsilver16384@gmail.com>
Change-Id: I5a20e403203d670df467ea97dde9a4ac40339a8d
diff --git a/util/label/BUILD.bazel b/util/label/BUILD.bazel
new file mode 100644
index 0000000..405006d
--- /dev/null
+++ b/util/label/BUILD.bazel
@@ -0,0 +1,16 @@
+load("//rust:defs.bzl", "rust_library", "rust_test")
+
+rust_library(
+ name = "label",
+ srcs = [
+ "label.rs",
+ "label_error.rs",
+ ],
+ edition = "2018",
+ visibility = ["//:__subpackages__"],
+)
+
+rust_test(
+ name = "label_test",
+ crate = ":label",
+)
diff --git a/util/label/label.rs b/util/label/label.rs
new file mode 100644
index 0000000..22d2bb4
--- /dev/null
+++ b/util/label/label.rs
@@ -0,0 +1,426 @@
+//! Bazel label parsing library.
+//!
+//! USAGE: `label::analyze("//foo/bar:baz")
+mod label_error;
+use label_error::LabelError;
+
+/// Parse and analyze given str.
+///
+/// TODO: validate . and .. in target name
+/// TODO: validate used characters in target name
+pub fn analyze(input: &'_ str) -> Result<Label<'_>> {
+ let label = input;
+ let (input, repository_name) = consume_repository_name(input, label)?;
+ let (input, package_name) = consume_package_name(input, label)?;
+ let name = consume_name(input, label)?;
+ let name = match (package_name, name) {
+ (None, None) => {
+ return Err(LabelError(err(
+ label,
+ "labels must have a package and/or a name.",
+ )))
+ }
+ (Some(package_name), None) => name_from_package(package_name),
+ (_, Some(name)) => name,
+ };
+ Ok(Label::new(repository_name, package_name, name))
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Label<'s> {
+ pub repository_name: Option<&'s str>,
+ pub package_name: Option<&'s str>,
+ pub name: &'s str,
+}
+
+type Result<T, E = LabelError> = core::result::Result<T, E>;
+
+impl<'s> Label<'s> {
+ fn new(
+ repository_name: Option<&'s str>,
+ package_name: Option<&'s str>,
+ name: &'s str,
+ ) -> Label<'s> {
+ Label {
+ repository_name,
+ package_name,
+ name,
+ }
+ }
+
+ pub fn packages(&self) -> Vec<&'s str> {
+ match self.package_name {
+ Some(name) => name.split('/').collect(),
+ None => vec![],
+ }
+ }
+}
+
+fn err<'s>(label: &'s str, msg: &'s str) -> String {
+ let mut err_msg = label.to_string();
+ err_msg.push_str(" must be a legal label; ");
+ err_msg.push_str(msg);
+ err_msg
+}
+
+fn consume_repository_name<'s>(
+ input: &'s str,
+ label: &'s str,
+) -> Result<(&'s str, Option<&'s str>)> {
+ if !input.starts_with('@') {
+ return Ok((input, None));
+ }
+
+ let slash_pos = input
+ .find("//")
+ .ok_or_else(|| err(label, "labels with repository must contain //."))?;
+ let repository_name = &input[1..slash_pos];
+ if repository_name.is_empty() {
+ return Ok((&input[1..], None));
+ }
+ if !repository_name
+ .chars()
+ .next()
+ .unwrap()
+ .is_ascii_alphabetic()
+ {
+ return Err(LabelError(err(
+ label,
+ "workspace names must start with a letter.",
+ )));
+ }
+ if !repository_name
+ .chars()
+ .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
+ {
+ return Err(LabelError(err(
+ label,
+ "workspace names \
+ may contain only A-Z, a-z, 0-9, '-', '_', and '.'.",
+ )));
+ }
+ Ok((&input[slash_pos..], Some(repository_name)))
+}
+
+fn consume_package_name<'s>(input: &'s str, label: &'s str) -> Result<(&'s str, Option<&'s str>)> {
+ let is_absolute = match input.rfind("//") {
+ None => false,
+ Some(0) => true,
+ Some(_) => {
+ return Err(LabelError(err(
+ label,
+ "'//' cannot appear in the middle of the label.",
+ )));
+ }
+ };
+
+ let (package_name, rest) = match (is_absolute, input.find(':')) {
+ (false, colon_pos) if colon_pos.map_or(true, |pos| pos != 0) => {
+ return Err(LabelError(err(
+ label,
+ "relative packages are not permitted.",
+ )));
+ }
+ (_, colon_pos) => {
+ let (input, colon_pos) = if is_absolute {
+ (&input[2..], colon_pos.map(|cp| cp - 2))
+ } else {
+ (input, colon_pos)
+ };
+ match colon_pos {
+ Some(colon_pos) => (&input[0..colon_pos], &input[colon_pos..]),
+ None => (input, ""),
+ }
+ }
+ };
+
+ if package_name.is_empty() {
+ return Ok((rest, None));
+ }
+
+ if !package_name.chars().all(|c| {
+ c.is_ascii_alphanumeric()
+ || c == '/'
+ || c == '-'
+ || c == '.'
+ || c == ' '
+ || c == '$'
+ || c == '('
+ || c == ')'
+ || c == '_'
+ }) {
+ return Err(LabelError(err(
+ label,
+ "package names may contain only A-Z, \
+ a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')' and '_'.",
+ )));
+ }
+ if package_name.ends_with('/') {
+ return Err(LabelError(err(
+ label,
+ "package names may not end with '/'.",
+ )));
+ }
+
+ if rest.is_empty() && is_absolute {
+ // This label doesn't contain the target name, we have to use
+ // last segment of the package name as target name.
+ return Ok((
+ match package_name.rfind('/') {
+ Some(pos) => &package_name[pos..],
+ None => package_name,
+ },
+ Some(package_name),
+ ));
+ }
+
+ Ok((rest, Some(package_name)))
+}
+
+fn consume_name<'s>(input: &'s str, label: &'s str) -> Result<Option<&'s str>> {
+ if input.is_empty() {
+ return Ok(None);
+ }
+ if input == ":" {
+ return Err(LabelError(err(label, "empty target name.")));
+ }
+ let name = input
+ .strip_prefix(':')
+ .or_else(|| input.strip_prefix('/'))
+ .unwrap_or(input);
+ if name.starts_with('/') {
+ return Err(LabelError(err(
+ label,
+ "target names may not start with '/'.",
+ )));
+ }
+ Ok(Some(name))
+}
+
+fn name_from_package(package_name: &str) -> &str {
+ package_name
+ .rsplit_once('/')
+ .map(|tup| tup.1)
+ .unwrap_or(package_name)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_new() {
+ assert_eq!(
+ Label::new(Some("repo"), Some("foo/bar"), "baz"),
+ Label {
+ repository_name: Some("repo"),
+ package_name: Some("foo/bar"),
+ name: "baz",
+ }
+ );
+ }
+
+ #[test]
+ fn test_repository_name_parsing() -> Result<()> {
+ assert_eq!(analyze("@repo//:foo")?.repository_name, Some("repo"));
+ assert_eq!(analyze("@//:foo")?.repository_name, None);
+ assert_eq!(analyze("//:foo")?.repository_name, None);
+ assert_eq!(analyze(":foo")?.repository_name, None);
+
+ assert_eq!(analyze("@repo//foo/bar")?.repository_name, Some("repo"));
+ assert_eq!(analyze("@//foo/bar")?.repository_name, None);
+ assert_eq!(analyze("//foo/bar")?.repository_name, None);
+ assert_eq!(
+ analyze("foo/bar"),
+ Err(LabelError(
+ "foo/bar must be a legal label; relative packages are not permitted.".to_string()
+ ))
+ );
+
+ assert_eq!(analyze("@repo//foo")?.repository_name, Some("repo"));
+ assert_eq!(analyze("@//foo")?.repository_name, None);
+ assert_eq!(analyze("//foo")?.repository_name, None);
+ assert_eq!(
+ analyze("foo"),
+ Err(LabelError(
+ "foo must be a legal label; relative packages are not permitted.".to_string()
+ ))
+ );
+
+ assert_eq!(
+ analyze("@foo:bar"),
+ Err(LabelError(
+ "@foo:bar must be a legal label; labels with repository must contain //."
+ .to_string()
+ ))
+ );
+
+ assert_eq!(
+ analyze("@AZab0123456789_-.//:foo")?.repository_name,
+ Some("AZab0123456789_-.")
+ );
+ assert_eq!(
+ analyze("@42//:baz"),
+ Err(LabelError(
+ "@42//:baz must be a legal label; workspace names must \
+ start with a letter."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("@foo#//:baz"),
+ Err(LabelError(
+ "@foo#//:baz must be a legal label; workspace names \
+ may contain only A-Z, a-z, 0-9, '-', '_', and '.'."
+ .to_string()
+ ))
+ );
+ Ok(())
+ }
+ #[test]
+ fn test_package_name_parsing() -> Result<()> {
+ assert_eq!(analyze("//:baz/qux")?.package_name, None);
+ assert_eq!(analyze(":baz/qux")?.package_name, None);
+
+ assert_eq!(analyze("//foo:baz/qux")?.package_name, Some("foo"));
+ assert_eq!(analyze("//foo/bar:baz/qux")?.package_name, Some("foo/bar"));
+ assert_eq!(
+ analyze("foo:baz/qux"),
+ Err(LabelError(
+ "foo:baz/qux must be a legal label; relative packages are not permitted."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("foo/bar:baz/qux"),
+ Err(LabelError(
+ "foo/bar:baz/qux must be a legal label; relative packages are not permitted."
+ .to_string()
+ ))
+ );
+
+ assert_eq!(analyze("//foo")?.package_name, Some("foo"));
+
+ assert_eq!(
+ analyze("foo//bar"),
+ Err(LabelError(
+ "foo//bar must be a legal label; '//' cannot appear in the middle of the label."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("//foo//bar"),
+ Err(LabelError(
+ "//foo//bar must be a legal label; '//' cannot appear in the middle of the label."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("foo//bar:baz"),
+ Err(LabelError(
+ "foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("//foo//bar:baz"),
+ Err(LabelError(
+ "//foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
+ .to_string()
+ ))
+ );
+
+ assert_eq!(
+ analyze("//azAZ09/-. $()_:baz")?.package_name,
+ Some("azAZ09/-. $()_")
+ );
+ assert_eq!(
+ analyze("//bar#:baz"),
+ Err(LabelError(
+ "//bar#:baz must be a legal label; package names may contain only A-Z, \
+ a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')' and '_'."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("//bar/:baz"),
+ Err(LabelError(
+ "//bar/:baz must be a legal label; package names may not end with '/'.".to_string()
+ ))
+ );
+
+ assert_eq!(analyze("@repo//foo/bar")?.package_name, Some("foo/bar"));
+ assert_eq!(analyze("//foo/bar")?.package_name, Some("foo/bar"));
+ assert_eq!(
+ analyze("foo/bar"),
+ Err(LabelError(
+ "foo/bar must be a legal label; relative packages are not permitted.".to_string()
+ ))
+ );
+
+ assert_eq!(analyze("@repo//foo")?.package_name, Some("foo"));
+ assert_eq!(analyze("//foo")?.package_name, Some("foo"));
+ assert_eq!(
+ analyze("foo"),
+ Err(LabelError(
+ "foo must be a legal label; relative packages are not permitted.".to_string()
+ ))
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_name_parsing() -> Result<()> {
+ assert_eq!(analyze("//foo:baz")?.name, "baz");
+ assert_eq!(analyze("//foo:baz/qux")?.name, "baz/qux");
+
+ assert_eq!(
+ analyze("//bar:"),
+ Err(LabelError(
+ "//bar: must be a legal label; empty target name.".to_string()
+ ))
+ );
+ assert_eq!(analyze("//foo")?.name, "foo");
+
+ assert_eq!(
+ analyze("//bar:/foo"),
+ Err(LabelError(
+ "//bar:/foo must be a legal label; target names may not start with '/'."
+ .to_string()
+ ))
+ );
+
+ assert_eq!(analyze("@repo//foo/bar")?.name, "bar");
+ assert_eq!(analyze("//foo/bar")?.name, "bar");
+ assert_eq!(
+ analyze("foo/bar"),
+ Err(LabelError(
+ "foo/bar must be a legal label; relative packages are not permitted.".to_string()
+ ))
+ );
+
+ assert_eq!(analyze("@repo//foo")?.name, "foo");
+ assert_eq!(analyze("//foo")?.name, "foo");
+ assert_eq!(
+ analyze("foo"),
+ Err(LabelError(
+ "foo must be a legal label; relative packages are not permitted.".to_string()
+ ))
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_packages() -> Result<()> {
+ assert_eq!(analyze("@repo//:baz")?.packages(), Vec::<&str>::new());
+ assert_eq!(analyze("@repo//foo:baz")?.packages(), vec!["foo"]);
+ assert_eq!(
+ analyze("@repo//foo/bar:baz")?.packages(),
+ vec!["foo", "bar"]
+ );
+
+ Ok(())
+ }
+}
diff --git a/util/label/label_error.rs b/util/label/label_error.rs
new file mode 100644
index 0000000..b1d7199
--- /dev/null
+++ b/util/label/label_error.rs
@@ -0,0 +1,20 @@
+#[derive(Debug, PartialEq)]
+pub struct LabelError(pub String);
+
+impl std::fmt::Display for LabelError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl std::error::Error for LabelError {
+ fn description(&self) -> &str {
+ &self.0
+ }
+}
+
+impl From<String> for LabelError {
+ fn from(msg: String) -> Self {
+ Self(msg)
+ }
+}