legacy: unescape all $u...$ that encode non-control Unicode codepoints.
diff --git a/src/legacy.rs b/src/legacy.rs
index 1e1824c..2050b8e 100644
--- a/src/legacy.rs
+++ b/src/legacy.rs
@@ -1,3 +1,4 @@
+use core::char;
 use core::fmt;
 
 /// Representation of a demangled symbol name.
@@ -133,7 +134,7 @@
             if rest.starts_with("_$") {
                 rest = &rest[1..];
             }
-            while !rest.is_empty() {
+            loop {
                 if rest.starts_with('.') {
                     if let Some('.') = rest[1..].chars().next() {
                         try!(f.write_str("::"));
@@ -143,55 +144,54 @@
                         rest = &rest[1..];
                     }
                 } else if rest.starts_with('$') {
-                    macro_rules! demangle {
-                        ($($pat:expr => $demangled:expr,)*) => ({
-                            $(if rest.starts_with($pat) {
-                                try!(f.write_str($demangled));
-                                rest = &rest[$pat.len()..];
-                              } else)*
-                            {
-                                try!(f.write_str(rest));
-                                break;
-                            }
-
-                        })
-                    }
-
-                    // see src/librustc/back/link.rs for these mappings
-                    demangle! {
-                        "$SP$" => "@",
-                        "$BP$" => "*",
-                        "$RF$" => "&",
-                        "$LT$" => "<",
-                        "$GT$" => ">",
-                        "$LP$" => "(",
-                        "$RP$" => ")",
-                        "$C$" => ",",
-
-                        // in theory we can demangle any Unicode code point, but
-                        // for simplicity we just catch the common ones.
-                        "$u7e$" => "~",
-                        "$u20$" => " ",
-                        "$u27$" => "'",
-                        "$u3d$" => "=",
-                        "$u5b$" => "[",
-                        "$u5d$" => "]",
-                        "$u7b$" => "{",
-                        "$u7d$" => "}",
-                        "$u3b$" => ";",
-                        "$u2b$" => "+",
-                        "$u21$" => "!",
-                        "$u22$" => "\"",
-                    }
-                } else {
-                    let idx = match rest.char_indices().find(|&(_, c)| c == '$' || c == '.') {
-                        None => rest.len(),
-                        Some((i, _)) => i,
+                    let (escape, after_escape) = if let Some(end) = rest[1..].find('$') {
+                        (&rest[1..end + 1], &rest[end + 2..])
+                    } else {
+                        break;
                     };
-                    try!(f.write_str(&rest[..idx]));
-                    rest = &rest[idx..];
+
+                    // see src/librustc_codegen_utils/symbol_names/legacy.rs for these mappings
+                    let unescaped = match escape {
+                        "SP" => "@",
+                        "BP" => "*",
+                        "RF" => "&",
+                        "LT" => "<",
+                        "GT" => ">",
+                        "LP" => "(",
+                        "RP" => ")",
+                        "C" => ",",
+
+                        _ => {
+                            if escape.starts_with('u') {
+                                let digits = &escape[1..];
+                                let all_lower_hex = digits.chars().all(|c| match c {
+                                    '0'...'9' | 'a'...'f' => true,
+                                    _ => false,
+                                });
+                                let c = u32::from_str_radix(digits, 16).ok()
+                                    .and_then(char::from_u32);
+                                if let (true, Some(c)) = (all_lower_hex, c) {
+                                    // FIXME(eddyb) do we need to filter out control codepoints?
+                                    if !c.is_control() {
+                                        try!(c.fmt(f));
+                                        rest = after_escape;
+                                        continue;
+                                    }
+                                }
+                            }
+                            break;
+                        }
+                    };
+                    try!(f.write_str(unescaped));
+                    rest = after_escape;
+                } else if let Some(i) = rest.find(|c| c == '$' || c == '.') {
+                    try!(f.write_str(&rest[..i]));
+                    rest = &rest[i..];
+                } else {
+                    break;
                 }
             }
+            try!(f.write_str(rest));
         }
 
         Ok(())
@@ -367,4 +367,20 @@
             "<core::result::Result<!, E> as std::process::Termination>::report::hfc41d0da4a40b3e8"
         );
     }
+
+    #[test]
+    fn demangle_utf8_idents() {
+        t_nohash!(
+            "_ZN11utf8_idents157_$u10e1$$u10d0$$u10ed$$u10db$$u10d4$$u10da$$u10d0$$u10d3$_$u10d2$$u10d4$$u10db$$u10e0$$u10d8$$u10d4$$u10da$$u10d8$_$u10e1$$u10d0$$u10d3$$u10d8$$u10da$$u10d8$17h21634fd5714000aaE",
+            "utf8_idents::საჭმელად_გემრიელი_სადილი"
+        );
+    }
+
+    #[test]
+    fn demangle_issue_60925() {
+        t_nohash!(
+            "_ZN11issue_609253foo37Foo$LT$issue_60925..llv$u6d$..Foo$GT$3foo17h059a991a004536adE",
+            "issue_60925::foo::Foo<issue_60925::llvm::Foo>::foo"
+        );
+    }
 }