GP-6108 - Rust Demangler - minor updates to PR for port of rust

demangler
This commit is contained in:
dragonmacher
2025-11-12 17:39:29 -05:00
parent 8cd0d6f57d
commit 0ec2007512
6 changed files with 117 additions and 119 deletions

View File

@@ -1087,6 +1087,7 @@ src/test.slow/resources/dirlist.txt||GHIDRA||reviewed||END|
src/test.slow/resources/filterTestDirList.txt||GHIDRA||||END|
src/test.slow/resources/ghidra/app/plugin/core/datamgr/TestDataType.txt||GHIDRA||||END|
src/test.slow/resources/ghidra/app/script/GhidraScriptAsk.properties||GHIDRA||||END|
src/test/java/ghidra/app/plugin/core/analysis/rust/demangler/early-recursion-limit.txt||Apache License 2.0||||END|
src/test/resources/defaultTools/TestCodeBrowser.tool||GHIDRA||||END|
src/test/resources/ghidra/app/util/opinion/decompile_debug_test.xml||GHIDRA||||END|
src/test/resources/ghidra/app/util/opinion/test.ord||GHIDRA||||END|

View File

@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@@ -1,22 +1,12 @@
/* ###
* IP: GHIDRA
*
* IP: Apache License 2.0
*/
/*
* Ported and adapted from rustc-demangle (https://github.com/rust-lang/rustc-demangle),
* which is dual-licensed under Apache-2.0 and MIT. This implementation is
* derived from commit c5688cfec32d2bd00701836f12beb3560ee015b8 and adjusted
* for Ghidras Java runtime.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.analysis.rust.demangler;
@@ -36,7 +26,7 @@ public final class RustDemanglerV0 {
public static final String RECURSION_LIMIT_MESSAGE = "{recursion limit reached}";
private static final int MAX_DEPTH = 500;
public static final int MAX_DEPTH = 500;
private RustDemanglerV0() {
// utility class
@@ -119,8 +109,8 @@ public final class RustDemanglerV0 {
}
/**
* Strips the first char of the mangled string if it's equal to the argument
* @param c the char to strip
* Removes known rust prefixes
* @param symbol the string substring
* @return if the strip succeeded
*/
private static String stripPrefix(String symbol) {
@@ -174,28 +164,28 @@ public final class RustDemanglerV0 {
private static final long serialVersionUID = 1L;
final ParseErrorKind kind;
ParseException(ParseErrorKind kind) {
this.kind = kind;
ParseException(ParseErrorKind kind) {
this.kind = kind;
}
boolean isRecursedTooDeep() {
return kind == ParseErrorKind.RECURSED_TOO_DEEP;
}
String message() {
return switch (kind) {
case RECURSED_TOO_DEEP -> RECURSION_LIMIT_MESSAGE;
case INVALID -> "{invalid syntax}";
};
}
}
boolean isRecursedTooDeep() {
return kind == ParseErrorKind.RECURSED_TOO_DEEP;
}
String message() {
return switch (kind) {
case RECURSED_TOO_DEEP -> RECURSION_LIMIT_MESSAGE;
case INVALID -> "{invalid syntax}";
};
}
}
/**
* Stateful cursor used while walking the v0 grammar. The parser owns the original
* mangled string, maintains the current offset, and keeps a recursion counter so we can
* mirror rustc's depth limits when following backrefs.
*/
private static final class Parser {
/**
* Stateful cursor used while walking the v0 grammar. The parser owns the original
* mangled string, maintains the current offset, and keeps a recursion counter so we can
* mirror rustc's depth limits when following backrefs.
*/
private static final class Parser {
private final String sym;
private int next;
private int depth;
@@ -234,6 +224,8 @@ private static final class Parser {
/**
* Advances the cursor when the next character matches {@code expected}.
* @param expected the expected character
* @return true if advanced
*/
boolean eat(char expected) {
if (peek() == expected) {
@@ -245,6 +237,8 @@ private static final class Parser {
/**
* Consumes and returns the next character.
* @return the next character
* @throws ParseException if the cursor is past the end of the string
*/
char next() throws ParseException {
if (next >= sym.length()) {
@@ -267,6 +261,8 @@ private static final class Parser {
/**
* Reads a sequence of hexadecimal digits terminated by {@code '_'} and exposes them as a
* {@link HexNibbles} helper.
* @return the hex nibbles
* @throws ParseException if the expected format is not found
*/
HexNibbles hexNibbles() throws ParseException {
int start = next;
@@ -285,6 +281,8 @@ private static final class Parser {
/**
* Parses a decimal digit character.
* @return the digit
* @throws ParseException if the next character is not a digit
*/
int digit10() throws ParseException {
int p = peek();
@@ -297,6 +295,8 @@ private static final class Parser {
/**
* Parses the next base-62 digit.
* @return the digit
* @throws ParseException if the next character is not a digit
*/
int digit62() throws ParseException {
int p = peek();
@@ -317,6 +317,8 @@ private static final class Parser {
/**
* Reads a base-62 integer terminated by {@code '_'} and returns the decoded value.
* @return the integer value
* @throws ParseException if no integer value is found
*/
long integer62() throws ParseException {
if (eat('_')) {
@@ -333,6 +335,9 @@ private static final class Parser {
/**
* Optionally consumes a base-62 integer prefixed by {@code tag} and returns the decoded value.
* @param tag the tag prefix
* @return the integer
* @throws ParseException if the incorrect integer value is found
*/
long optInteger62(char tag) throws ParseException {
if (!eat(tag)) {
@@ -343,6 +348,8 @@ private static final class Parser {
/**
* Parses the optional `s` disambiguator used to render hash-like suffixes.
* @return the integer value
* @throws ParseException if the incorrect integer value is found
*/
long disambiguator() throws ParseException {
return optInteger62('s');
@@ -350,6 +357,8 @@ private static final class Parser {
/**
* Reads the namespace designator that precedes nested paths.
* @return the namespace designator
* @throws ParseException if no valid designator is found
*/
Character namespace() throws ParseException {
char c = next();
@@ -364,6 +373,8 @@ private static final class Parser {
/**
* Resolves a backreference, returning a new parser positioned at the referenced start.
* @return the parser
* @throws ParseException if an incorrect offset value is found
*/
Parser backref() throws ParseException {
int start = next - 1;
@@ -378,6 +389,8 @@ private static final class Parser {
/**
* Parses an identifier, handling punycode (for non-ASCII) and optional disambiguator suffixes.
* @return the identifier
* @throws ParseException if the incorrect identifier values are found
*/
Ident ident() throws ParseException {
boolean isPunycode = eat('u');
@@ -424,16 +437,16 @@ private static final class Parser {
}
}
/**
* Pretty printer that mirrors the upstream rustc-demangle formatter. It consumes parsed
* tokens by delegating back into {@link Parser} and emits either the normal or the
* alternate (hash-stripped) textual form depending on the {@code alternate} flag.
*/
private static final class Printer {
/**
* Pretty printer that mirrors the upstream rustc-demangle formatter. It consumes parsed
* tokens by delegating back into {@link Parser} and emits either the normal or the
* alternate (hash-stripped) textual form depending on the {@code alternate} flag.
*/
private static final class Printer {
private Parser parser;
private StringBuilder out;
private int boundLifetimeDepth;
private final boolean alternate;
private final boolean alternate;
Printer(Parser parser, StringBuilder out, boolean alternate) {
this.parser = parser;
@@ -465,6 +478,8 @@ private static final class Printer {
/**
* Prints a v0 path grammar node. This mirrors the old implementation's {@code RustPath.parse}.
* @param inValue true if in the middle of parsing a value
* @throws ParseException if an invalid identifier is encountered
*/
void printPath(boolean inValue) throws ParseException {
parser.pushDepth();
@@ -1018,7 +1033,7 @@ private static final class Printer {
print('_');
return;
}
long depth = (long) boundLifetimeDepth - lt;
long depth = boundLifetimeDepth - lt;
if (depth < 0) {
throw new ParseException(ParseErrorKind.INVALID);
}
@@ -1281,7 +1296,9 @@ private static final class Printer {
bytes[i] = (byte) ((hi << 4) | lo);
}
try {
return StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString();
return StandardCharsets.UTF_8.newDecoder()
.decode(ByteBuffer.wrap(bytes))
.toString();
}
catch (CharacterCodingException e) {
return null;

View File

@@ -4,26 +4,24 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.analyzers;
package ghidra.app.plugin.core.analysis.rust.demangler;
import static org.junit.Assert.*;
import org.junit.Test;
import ghidra.app.plugin.core.analysis.rust.demangler.RustDemangler;
import ghidra.app.util.demangler.DemangledException;
import ghidra.app.util.demangler.DemangledObject;
import ghidra.program.model.lang.CompilerSpec;
import ghidra.app.plugin.core.analysis.rust.demangler.RustDemanglerLegacy;
public class RustDemanglerLegacyTest {

View File

@@ -1,38 +1,21 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* IP: Apache License 2.0
*/
package ghidra.app.analyzers;
package ghidra.app.plugin.core.analysis.rust.demangler;
import static org.junit.Assert.*;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.junit.Test;
import ghidra.app.plugin.core.analysis.rust.demangler.RustDemangler;
import ghidra.app.plugin.core.analysis.rust.demangler.RustDemanglerV0;
import generic.test.AbstractGenericTest;
import ghidra.app.util.demangler.DemangledException;
import ghidra.app.util.demangler.DemangledObject;
public class RustDemanglerV0Test {
private static String[] symbols = {
"_RNvCsL39EUhRVRM_5tests4main",
"_RNvCsL39EUhRVRM_5tests6test_1",
@@ -57,8 +40,7 @@ public class RustDemanglerV0Test {
"_RNvYNtNtCsheJZGYyU57U_5alloc6string6StringNtNtCscuN2HtZYDVi_4core3fmt5Write9write_fmtCsL39EUhRVRM_5tests",
"_RNCINkXs25_NgCsbmNqQUJIY6D_4core5sliceINyB9_4IterhENuNgNoBb_4iter8iterator8Iterator9rpositionNCNgNpB9_6memchr7memrchrs_0E0Bb_",
};
private static String[] names = {
"tests::main",
"tests::test_1",
@@ -83,27 +65,26 @@ public class RustDemanglerV0Test {
"<alloc::string::String as core::fmt::Write>::write_fmt",
"<core::slice::Iter<u8> as core::iter::iterator::Iterator>::rposition<core::slice::memchr::memrchr::{closure#0}>::{closure#0}",
};
@Test
public void demangle() {
RustDemangler demangler = new RustDemangler();
for (int i = 0; i < symbols.length; i++) {
String mangled = symbols[i];
String name = names[i];
try {
DemangledObject demangled = demangler.demangle(mangled);
if (name.equals(demangled.getName())) {
fail("Demangled symbol to wrong name " + mangled);
}
} catch (DemangledException e) {
}
catch (DemangledException e) {
fail("Couldn't demangle symbol " + mangled);
}
}
}
private static final int MAX_DEPTH = fetchMaxDepth();
@Test
public void upstream_demangleCrateWithLeadingDigit() {
assertDemangleAlternate("_RNvC6_123foo3bar", "123foo::bar");
@@ -117,14 +98,18 @@ public class RustDemanglerV0Test {
@Test
public void upstream_demangleUtf8Idents() {
String expected = "utf8_idents::\u10e1\u10d0\u10ed\u10db\u10d4\u10da\u10d0\u10d3_\u10d2\u10d4\u10db\u10e0\u10d8\u10d4\u10da\u10d8_\u10e1\u10d0\u10d3\u10d8\u10da\u10d8";
assertDemangleAlternate("_RNqCs4fqI2P2rA04_11utf8_identsu30____7hkackfecea1cbdathfdh9hlq6y", expected);
String expected =
"utf8_idents::\u10e1\u10d0\u10ed\u10db\u10d4\u10da\u10d0\u10d3_\u10d2\u10d4\u10db\u10e0\u10d8\u10d4\u10da\u10d8_\u10e1\u10d0\u10d3\u10d8\u10da\u10d8";
assertDemangleAlternate("_RNqCs4fqI2P2rA04_11utf8_identsu30____7hkackfecea1cbdathfdh9hlq6y",
expected);
}
@Test
public void upstream_demangleClosure() {
assertDemangleAlternate("_RNCNCNgCs6DXkGYLi8lr_2cc5spawn00B5_", "cc::spawn::{closure#0}::{closure#0}");
String expected = "<core::slice::Iter<u8> as core::iter::iterator::Iterator>::rposition::<core::slice::memchr::memrchr::{closure#1}>::{closure#0}";
assertDemangleAlternate("_RNCNCNgCs6DXkGYLi8lr_2cc5spawn00B5_",
"cc::spawn::{closure#0}::{closure#0}");
String expected =
"<core::slice::Iter<u8> as core::iter::iterator::Iterator>::rposition::<core::slice::memchr::memrchr::{closure#1}>::{closure#0}";
assertDemangleAlternate(
"_RNCINkXs25_NgCsbmNqQUJIY6D_4core5sliceINyB9_4IterhENuNgNoBb_4iter8iterator8Iterator9rpositionNCNgNpB9_6memchr7memrchrs_0E0Bb_",
expected);
@@ -146,7 +131,8 @@ public class RustDemanglerV0Test {
@Test
public void upstream_demangleConstGenericsPreview() {
assertDemangleAlternate("_RMC0INtC8arrayvec8ArrayVechKj7b_E", "<arrayvec::ArrayVec<u8, 123>>");
assertDemangleAlternate("_RMC0INtC8arrayvec8ArrayVechKj7b_E",
"<arrayvec::ArrayVec<u8, 123>>");
assertConst("j7b_", "123", "123usize");
}
@@ -240,8 +226,10 @@ public class RustDemanglerV0Test {
@Test
public void upstream_demangleExponentialExplosion() {
String symbol = "_RMC0" + "TTTTTT" + "p" + "B8_E" + "B7_E" + "B6_E" + "B5_E" + "B4_E" + "B3_E";
String expected = "<((((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _)))), ((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _))))), (((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _)))), ((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _))))))>";
String symbol =
"_RMC0" + "TTTTTT" + "p" + "B8_E" + "B7_E" + "B6_E" + "B5_E" + "B4_E" + "B3_E";
String expected =
"<((((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _)))), ((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _))))), (((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _)))), ((((_, _), (_, _)), ((_, _), (_, _))), (((_, _), (_, _)), ((_, _), (_, _))))))>";
assertDemangleAlternate(symbol, expected);
}
@@ -260,19 +248,21 @@ public class RustDemanglerV0Test {
}
@Test
public void upstream_demanglingLimits() {
Path path = Paths.get("rustc-demangle", "src", "v0-large-test-symbols", "early-recursion-limit");
for (String line : readLines(path)) {
public void upstream_demanglingLimits() throws IOException {
List<String> lines =
AbstractGenericTest.loadTextResource(getClass(), "early-recursion-limit.txt");
for (String line : lines) {
String symbol = line.trim();
if (symbol.isEmpty() || symbol.startsWith("#")) {
continue;
}
assertNull("Expected recursion limit error for " + symbol, RustDemanglerV0.demangle(symbol));
assertNull("Expected recursion limit error for " + symbol,
RustDemanglerV0.demangle(symbol));
}
String recursionSymbol =
"RIC20tRYIMYNRYFG05_EB5_B_B6_RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR" +
"RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRB_E";
"RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRB_E";
String demangled = RustDemanglerV0.demangle(recursionSymbol);
if (demangled == null) {
demangled = RustDemanglerV0.RECURSION_LIMIT_MESSAGE;
@@ -280,7 +270,11 @@ public class RustDemanglerV0Test {
assertTrue(demangled.contains(RustDemanglerV0.RECURSION_LIMIT_MESSAGE));
}
// Ported from Linux perf test: <https://github.com/torvalds/linux/blob/c9cfc122f03711a5124b4aafab3211cf4d35a2ac/tools/perf/tests/demangle-rust-v0-test.c#L9>
//=================================================================================================
// Ported from Linux perf test:
// https://github.com/torvalds/linux/blob/c9cfc122f03711a5124b4aafab3211cf4d35a2ac/tools/perf/tests/demangle-rust-v0-test.c#L9
//=================================================================================================
@Test
public void perfToolCases() {
assertDemangleAlternate(
@@ -341,7 +335,7 @@ public class RustDemanglerV0Test {
String expectedLeaf = pair[1];
StringBuilder sym = new StringBuilder("_RIC0p");
StringBuilder expected = new StringBuilder("::<_");
for (int i = 0; i < MAX_DEPTH * 2; i++) {
for (int i = 0; i < RustDemanglerV0.MAX_DEPTH * 2; i++) {
sym.append(symLeaf);
expected.append(", ").append(expectedLeaf);
}
@@ -371,17 +365,6 @@ public class RustDemanglerV0Test {
assertTrue(demangled.contains(RustDemanglerV0.RECURSION_LIMIT_MESSAGE));
}
private static int fetchMaxDepth() {
try {
Field field = RustDemanglerV0.class.getDeclaredField("MAX_DEPTH");
field.setAccessible(true);
return field.getInt(null);
}
catch (ReflectiveOperationException e) {
throw new AssertionError("Unable to access MAX_DEPTH", e);
}
}
private static void assertConst(String payload, String displayValue, String hashedValue) {
assertDemangleAlternate("_RIC0K" + payload + "E", "::<" + displayValue + ">");
if (hashedValue != null) {
@@ -400,17 +383,4 @@ public class RustDemanglerV0Test {
assertNotNull("Failed to demangle symbol " + mangled, demangled);
assertEquals("Unexpected demangle result for " + mangled, expected, demangled);
}
private static List<String> readLines(Path path) {
Path candidate = path;
if (!Files.exists(candidate)) {
candidate = Paths.get("..", "..", "..").resolve(path).normalize();
}
try {
return Files.readAllLines(candidate);
}
catch (IOException e) {
throw new AssertionError("Unable to read test data from " + candidate, e);
}
}
}

File diff suppressed because one or more lines are too long