Merge pull request #62 from Mark-Simulacrum/stream-api

Support searching for and demangling symbols
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6ae8d0c..4397394 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -14,6 +14,7 @@
       run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }}
     - run: cargo build --all
     - run: cargo test --all
+    - run: cargo build --features std
 
   fuzz_targets:
     name: Fuzz Targets
@@ -23,7 +24,7 @@
     # Note that building with fuzzers requires nightly since it uses unstable
     # flags to rustc.
     - run: rustup update nightly && rustup default nightly
-    - run: cargo install cargo-fuzz --vers "^0.10"
+    - run: cargo install cargo-fuzz --vers "^0.11"
     - run: cargo fuzz build --dev
 
   rustfmt:
diff --git a/Cargo.toml b/Cargo.toml
index 552e069..1deb42f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rustc-demangle"
-version = "0.1.21"
+version = "0.1.22"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 license = "MIT/Apache-2.0"
 readme = "README.md"
@@ -20,6 +20,11 @@
 
 [features]
 rustc-dep-of-std = ['core', 'compiler_builtins']
+std = []
 
 [profile.release]
 lto = true
+
+[package.metadata.docs.rs]
+features = ["std"]
+rustdoc-args = ["--cfg", "docsrs"]
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 4b7533f..717a322 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -10,7 +10,7 @@
 
 [dependencies]
 libfuzzer-sys = "0.4"
-rustc-demangle = { path = '..' }
+rustc-demangle = { path = '..', features = ["std"] }
 
 [[bin]]
 name = "demangle"
diff --git a/fuzz/fuzz_targets/demangle.rs b/fuzz/fuzz_targets/demangle.rs
index c1f7e87..e41ae00 100644
--- a/fuzz/fuzz_targets/demangle.rs
+++ b/fuzz/fuzz_targets/demangle.rs
@@ -11,4 +11,17 @@
     if let Ok(sym) = rustc_demangle::try_demangle(data) {
         drop(write!(s, "{}", sym));
     }
+
+    let mut output = Vec::new();
+    drop(rustc_demangle::demangle_stream(
+        &mut s.as_bytes(),
+        &mut output,
+        true,
+    ));
+    output.clear();
+    drop(rustc_demangle::demangle_stream(
+        &mut s.as_bytes(),
+        &mut output,
+        false,
+    ));
 });
diff --git a/src/lib.rs b/src/lib.rs
index 1ecb13f..7eb1c42 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -25,8 +25,9 @@
 
 #![no_std]
 #![deny(missing_docs)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
 
-#[cfg(test)]
+#[cfg(any(test, feature = "std"))]
 #[macro_use]
 extern crate std;
 
@@ -144,6 +145,74 @@
     }
 }
 
+#[cfg(feature = "std")]
+fn demangle_line(line: &str, include_hash: bool) -> std::borrow::Cow<str> {
+    let mut line = std::borrow::Cow::Borrowed(line);
+    let mut head = 0;
+    loop {
+        // Move to the next potential match
+        head = match (line[head..].find("_ZN"), line[head..].find("_R")) {
+            (Some(idx), None) | (None, Some(idx)) => head + idx,
+            (Some(idx1), Some(idx2)) => head + idx1.min(idx2),
+            (None, None) => {
+                // No more matches, we can return our line.
+                return line;
+            }
+        };
+        // Find the non-matching character.
+        //
+        // If we do not find a character, then until the end of the line is the
+        // thing to demangle.
+        let match_end = line[head..]
+            .find(|ch: char| !(ch == '$' || ch == '.' || ch == '_' || ch.is_ascii_alphanumeric()))
+            .map(|idx| head + idx)
+            .unwrap_or(line.len());
+
+        let mangled = &line[head..match_end];
+        if let Ok(demangled) = try_demangle(mangled) {
+            let demangled = if include_hash {
+                format!("{}", demangled)
+            } else {
+                format!("{:#}", demangled)
+            };
+            line.to_mut().replace_range(head..match_end, &demangled);
+            // Start again after the replacement.
+            head = head + demangled.len();
+        } else {
+            // Skip over the full symbol. We don't try to find a partial Rust symbol in the wider
+            // matched text today.
+            head = head + mangled.len();
+        }
+    }
+}
+
+/// Process a stream of data from `input` into the provided `output`, demangling any symbols found
+/// within.
+///
+/// This currently is implemented by buffering each line of input in memory, but that may be
+/// changed in the future. Symbols never cross line boundaries so this is just an implementation
+/// detail.
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+pub fn demangle_stream<R: std::io::BufRead, W: std::io::Write>(
+    input: &mut R,
+    output: &mut W,
+    include_hash: bool,
+) -> std::io::Result<()> {
+    let mut buf = std::string::String::new();
+    // We read in lines to reduce the memory usage at any time.
+    //
+    // demangle_line is also more efficient with relatively small buffers as it will copy around
+    // trailing data during demangling. In the future we might directly stream to the output but at
+    // least right now that seems to be less efficient.
+    while input.read_line(&mut buf)? > 0 {
+        let demangled_line = demangle_line(&buf, include_hash);
+        output.write_all(demangled_line.as_bytes())?;
+        buf.clear();
+    }
+    Ok(())
+}
+
 /// Error returned from the `try_demangle` function below when demangling fails.
 #[derive(Debug, Clone)]
 pub struct TryDemangleError {
@@ -490,4 +559,22 @@
             "{size limit reached}"
         );
     }
+
+    #[test]
+    #[cfg(feature = "std")]
+    fn find_multiple() {
+        assert_eq!(
+            super::demangle_line("_ZN3fooE.llvm moocow _ZN3fooE.llvm", false),
+            "foo.llvm moocow foo.llvm"
+        );
+    }
+
+    #[test]
+    #[cfg(feature = "std")]
+    fn interleaved_new_legacy() {
+        assert_eq!(
+            super::demangle_line("_ZN3fooE.llvm moocow _RNvMNtNtNtNtCs8a2262Dv4r_3mio3sys4unix8selector5epollNtB2_8Selector6select _ZN3fooE.llvm", false),
+            "foo.llvm moocow <mio::sys::unix::selector::epoll::Selector>::select foo.llvm"
+        );
+    }
 }