Added utility library for parsing workspace status stamps. (#2982)

This change introduces a small utility for parsing workspace status
stamp files. I found similar snippets throughout projects I work on and
thought consolidation within `rules_rust` would be good. An example
usage can be seen here:

```rust
use workspace_status::parse_workspace_status_stamps;

fn main() {
    let stable_status = std::fs::read_to_string("bazel-out/stable-status.txt").unwrap();
    let volatile_status = std::fs::read_to_string("bazel-out/volatile-status.txt").unwrap();
    let stamps = parse_workspace_status_stamps(&stable_status)
                .chain(parse_workspace_status_stamps(&stable_status))
                .flatten()
                .collect::<BTreeMap<_, _>>();
    // ...
    // ...
    // ...
}
```
diff --git a/tools/workspace_status/BUILD.bazel b/tools/workspace_status/BUILD.bazel
new file mode 100644
index 0000000..c207cf9
--- /dev/null
+++ b/tools/workspace_status/BUILD.bazel
@@ -0,0 +1,13 @@
+load("//rust:defs.bzl", "rust_library", "rust_test")
+
+rust_library(
+    name = "workspace_status",
+    srcs = ["workspace_status.rs"],
+    edition = "2021",
+    visibility = ["//visibility:public"],
+)
+
+rust_test(
+    name = "workspace_status_test",
+    crate = ":workspace_status",
+)
diff --git a/tools/workspace_status/workspace_status.rs b/tools/workspace_status/workspace_status.rs
new file mode 100644
index 0000000..8f05879
--- /dev/null
+++ b/tools/workspace_status/workspace_status.rs
@@ -0,0 +1,121 @@
+//! Utilities for parsing [workspace status stamps](https://bazel.build/docs/user-manual#workspace-status).
+
+/// The error type of workspace status parsing.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum WorkspaceStatusError {
+    /// The workspace status data is malformed and cannot be parsed.
+    InvalidFormat(String),
+}
+
+/// Returns an iterator of workspace status stamp values parsed from the given text.
+pub fn parse_workspace_status_stamps(
+    text: &'_ str,
+) -> impl Iterator<Item = Result<(&'_ str, &'_ str), WorkspaceStatusError>> {
+    text.lines().map(|l| {
+        let pair = l.split_once(' ');
+        pair.ok_or_else(|| {
+            WorkspaceStatusError::InvalidFormat(format!(
+                "Invalid workspace status stamp value:\n{}",
+                l
+            ))
+        })
+    })
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    use std::collections::{BTreeMap, HashMap};
+
+    #[test]
+    fn test_parse_into_btree_map() {
+        let status = [
+            "BUILD_TIMESTAMP 1730574875",
+            "BUILD_USER user name",
+            "STABLE_STAMP_VALUE stable",
+        ]
+        .join("\n");
+
+        let stamps = parse_workspace_status_stamps(&status)
+            .flatten()
+            .collect::<BTreeMap<_, _>>();
+        assert_eq!(
+            BTreeMap::from([
+                ("BUILD_TIMESTAMP", "1730574875"),
+                ("BUILD_USER", "user name"),
+                ("STABLE_STAMP_VALUE", "stable"),
+            ]),
+            stamps
+        );
+    }
+
+    #[test]
+    fn test_parse_into_hash_map() {
+        let status = [
+            "BUILD_TIMESTAMP 1730574875",
+            "BUILD_USER user name",
+            "STABLE_STAMP_VALUE stable",
+        ]
+        .join("\n");
+
+        let stamps = parse_workspace_status_stamps(&status)
+            .flatten()
+            .collect::<HashMap<_, _>>();
+        assert_eq!(
+            HashMap::from([
+                ("BUILD_TIMESTAMP", "1730574875"),
+                ("BUILD_USER", "user name"),
+                ("STABLE_STAMP_VALUE", "stable"),
+            ]),
+            stamps
+        );
+    }
+
+    #[test]
+    fn test_chain() {
+        let stable_status =
+            ["STABLE_STAMP_VALUE1 stable1", "STABLE_STAMP_VALUE2 stable2"].join("\n");
+
+        let volatile_status = [
+            "VOLATILE_STAMP_VALUE1 volatile1",
+            "VOLATILE_STAMP_VALUE2 volatile2",
+        ]
+        .join("\n");
+
+        let stamps = parse_workspace_status_stamps(&stable_status)
+            .chain(parse_workspace_status_stamps(&volatile_status))
+            .flatten()
+            .collect::<BTreeMap<_, _>>();
+
+        assert_eq!(
+            BTreeMap::from([
+                ("STABLE_STAMP_VALUE1", "stable1"),
+                ("STABLE_STAMP_VALUE2", "stable2"),
+                ("VOLATILE_STAMP_VALUE1", "volatile1"),
+                ("VOLATILE_STAMP_VALUE2", "volatile2"),
+            ]),
+            stamps
+        );
+    }
+
+    #[test]
+    fn test_parse_invalid_stamps() {
+        let status = [
+            "BUILD_TIMESTAMP 1730574875",
+            "BUILD_USERusername",
+            "STABLE_STAMP_VALUE stable",
+        ]
+        .join("\n");
+
+        let error = parse_workspace_status_stamps(&status)
+            .find_map(|result| result.err())
+            .expect("No error found when one was expected");
+        assert_eq!(
+            WorkspaceStatusError::InvalidFormat(
+                "Invalid workspace status stamp value:\nBUILD_USERusername".to_owned()
+            ),
+            error
+        );
+    }
+}