Skip to content

Commit 1e6a0a0

Browse files
committed
[Rust] Misc QualifiedName cleanup
- Added some unit tests - Improved documentation - Made `QualifiedName::new` and `QualifiedName::new_with_separator` generic over `Into<String>`
1 parent aec0d26 commit 1e6a0a0

File tree

2 files changed

+239
-9
lines changed

2 files changed

+239
-9
lines changed

rust/src/qualified_name.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
1+
//! The [`QualifiedName`] is the canonical way to represent structured names in Binary Ninja.
2+
13
use crate::rc::{CoreArrayProvider, CoreArrayProviderInner};
24
use crate::string::{raw_to_string, strings_to_string_list, BnString};
35
use binaryninjacore_sys::*;
46
use std::borrow::Cow;
57
use std::fmt::{Display, Formatter};
68
use std::ops::{Index, IndexMut};
79

8-
// TODO: Document usage, specifically how to make a qualified name and why it exists.
10+
/// A [`QualifiedName`] represents a name composed of multiple components, typically used for symbols
11+
/// and type names within namespaces, classes, or modules.
12+
///
13+
/// # Creating a Qualified Name
14+
///
15+
/// ```
16+
/// use binaryninja::qualified_name::QualifiedName;
17+
///
18+
/// // Uses the default separator "::"
19+
/// let qn_vec = QualifiedName::new(vec!["my", "namespace", "func"]);
20+
/// assert_eq!(qn_vec.to_string(), "my::namespace::func");
21+
///
22+
/// // Using `QualifiedName::from` will not split on the default separator "::".
23+
/// let qn_from = QualifiedName::from("std::string");
24+
/// assert_eq!(qn_from.len(), 1);
25+
/// assert_eq!(qn_from.to_string(), "std::string");
26+
/// ```
27+
///
28+
/// # Using a Custom Separator
29+
///
30+
/// While `::` is the default, you can specify a custom separator:
31+
///
32+
/// ```
33+
/// use binaryninja::qualified_name::QualifiedName;
34+
///
35+
/// let qn = QualifiedName::new_with_separator(["a", "b", "c"], ".");
36+
/// assert_eq!(qn.to_string(), "a.b.c");
37+
/// ```
938
#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
1039
pub struct QualifiedName {
1140
// TODO: Make this Option<String> where default is "::".
@@ -14,7 +43,7 @@ pub struct QualifiedName {
1443
}
1544

1645
impl QualifiedName {
17-
pub(crate) fn from_raw(value: &BNQualifiedName) -> Self {
46+
pub fn from_raw(value: &BNQualifiedName) -> Self {
1847
// TODO: This could be improved...
1948
let raw_names = unsafe { std::slice::from_raw_parts(value.name, value.nameCount) };
2049
let items = raw_names
@@ -25,7 +54,7 @@ impl QualifiedName {
2554
Self { items, separator }
2655
}
2756

28-
pub(crate) fn from_owned_raw(value: BNQualifiedName) -> Self {
57+
pub fn from_owned_raw(value: BNQualifiedName) -> Self {
2958
let result = Self::from_raw(&value);
3059
Self::free_raw(value);
3160
result
@@ -42,17 +71,31 @@ impl QualifiedName {
4271
}
4372
}
4473

45-
pub(crate) fn free_raw(value: BNQualifiedName) {
74+
pub fn free_raw(value: BNQualifiedName) {
4675
unsafe { BnString::free_raw(value.join) };
4776
unsafe { BNFreeStringList(value.name, value.nameCount) };
4877
}
4978

50-
pub fn new(items: Vec<String>) -> Self {
51-
Self::new_with_separator(items, "::".to_string())
79+
/// Creates a new [`QualifiedName`] with the default separator `::`.
80+
pub fn new<I, S>(items: I) -> Self
81+
where
82+
I: IntoIterator<Item = S>,
83+
S: Into<String>,
84+
{
85+
Self::new_with_separator(items, "::")
5286
}
5387

54-
pub fn new_with_separator(items: Vec<String>, separator: String) -> Self {
55-
Self { items, separator }
88+
/// Creates a new `QualifiedName` with a custom separator.
89+
pub fn new_with_separator<I, S>(items: I, separator: impl Into<String>) -> Self
90+
where
91+
I: IntoIterator<Item = S>,
92+
S: Into<String>,
93+
{
94+
let items = items.into_iter().map(Into::into).collect::<Vec<String>>();
95+
Self {
96+
items,
97+
separator: separator.into(),
98+
}
5699
}
57100

58101
pub fn with_item(&self, item: impl Into<String>) -> Self {
@@ -90,7 +133,7 @@ impl QualifiedName {
90133
/// # Example
91134
///
92135
/// ```
93-
/// use binaryninja::types::QualifiedName;
136+
/// use binaryninja::qualified_name::QualifiedName;
94137
///
95138
/// let qualified_name =
96139
/// QualifiedName::new(vec!["my::namespace".to_string(), "mytype".to_string()]);

rust/tests/qualified_name.rs

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
use binaryninja::qualified_name::QualifiedName;
2+
3+
#[test]
4+
fn test_new_from_vec_string() {
5+
let items = vec!["std".to_string(), "vector".to_string(), "int".to_string()];
6+
let qn = QualifiedName::new(items);
7+
assert_eq!(qn.len(), 3);
8+
assert_eq!(qn.to_string(), "std::vector::int");
9+
assert_eq!(qn.separator, "::");
10+
}
11+
12+
#[test]
13+
fn test_new_from_array() {
14+
let items = ["root", "data"];
15+
let qn = QualifiedName::new(items);
16+
assert_eq!(qn.len(), 2);
17+
assert_eq!(qn.to_string(), "root::data");
18+
}
19+
20+
#[test]
21+
fn test_new_empty() {
22+
let items: Vec<String> = Vec::new();
23+
let qn = QualifiedName::new(items);
24+
assert!(qn.is_empty());
25+
assert_eq!(qn.to_string(), "");
26+
}
27+
28+
#[test]
29+
fn test_new_with_custom_separator() {
30+
let items = vec!["a".to_string(), "b".to_string(), "c".to_string()];
31+
let separator = ".".to_string();
32+
let qn = QualifiedName::new_with_separator(items, separator);
33+
assert_eq!(qn.to_string(), "a.b.c");
34+
assert_eq!(qn.separator, ".");
35+
}
36+
37+
#[test]
38+
fn test_from_string() {
39+
let qn: QualifiedName = "SingleName".to_string().into();
40+
assert_eq!(qn.len(), 1);
41+
assert_eq!(qn[0], "SingleName");
42+
assert_eq!(qn.to_string(), "SingleName");
43+
}
44+
45+
#[test]
46+
fn test_from_str_literal() {
47+
let qn: QualifiedName = "AnotherName".into();
48+
assert_eq!(qn.len(), 1);
49+
assert_eq!(qn[0], "AnotherName");
50+
}
51+
52+
#[test]
53+
fn test_into_string() {
54+
let qn = QualifiedName::new(vec!["A", "B", "C"]);
55+
let s: String = qn.into();
56+
assert_eq!(s, "A::B::C");
57+
}
58+
59+
#[test]
60+
fn test_push_pop_and_last() {
61+
let mut qn = QualifiedName::new(vec!["ns1"]);
62+
63+
qn.push("TypeA".to_string());
64+
assert_eq!(qn.len(), 2);
65+
assert_eq!(qn.to_string(), "ns1::TypeA");
66+
67+
assert_eq!(qn.last().unwrap(), "TypeA");
68+
*qn.last_mut().unwrap() = "TypeB".to_string();
69+
assert_eq!(qn.to_string(), "ns1::TypeB");
70+
71+
assert_eq!(qn.pop().unwrap(), "TypeB");
72+
assert_eq!(qn.len(), 1);
73+
assert_eq!(qn.to_string(), "ns1");
74+
75+
assert_eq!(qn.pop().unwrap(), "ns1");
76+
assert!(qn.is_empty());
77+
assert_eq!(qn.pop(), None);
78+
}
79+
80+
#[test]
81+
fn test_with_item() {
82+
let qn_a = QualifiedName::from("ns1");
83+
let qn_b = qn_a.with_item("ns2");
84+
let qn_c = qn_b.with_item("symbol");
85+
86+
assert_eq!(qn_a.to_string(), "ns1");
87+
assert_eq!(qn_b.to_string(), "ns1::ns2");
88+
assert_eq!(qn_c.to_string(), "ns1::ns2::symbol");
89+
90+
// Ensure the original is unchanged
91+
assert_eq!(qn_a.len(), 1);
92+
}
93+
94+
#[test]
95+
fn test_split_last() {
96+
let qn = QualifiedName::new(vec!["A", "B", "C"]);
97+
98+
let (last, prefix) = qn.split_last().unwrap();
99+
assert_eq!(last, "C");
100+
assert_eq!(prefix.to_string(), "A::B");
101+
102+
let (last2, prefix2) = prefix.split_last().unwrap();
103+
assert_eq!(last2, "B");
104+
assert_eq!(prefix2.to_string(), "A");
105+
106+
let (last3, prefix3) = prefix2.split_last().unwrap();
107+
assert_eq!(last3, "A");
108+
assert!(prefix3.is_empty());
109+
110+
// Split on an empty name
111+
assert_eq!(prefix3.split_last(), None);
112+
}
113+
114+
#[test]
115+
fn test_replace() {
116+
let qualified_name =
117+
QualifiedName::new(vec!["my_prefix::ns".to_string(), "my_Type".to_string()]);
118+
119+
let replaced = qualified_name.replace("my", "your");
120+
assert_eq!(
121+
replaced.items,
122+
vec!["your_prefix::ns".to_string(), "your_Type".to_string()]
123+
);
124+
assert_eq!(replaced.to_string(), "your_prefix::ns::your_Type");
125+
126+
let qualified_name_dot = QualifiedName::new_with_separator(vec!["foo", "bar"], ".".to_string());
127+
let replaced_dot = qualified_name_dot.replace("foo", "baz");
128+
assert_eq!(replaced_dot.to_string(), "baz.bar");
129+
}
130+
131+
#[test]
132+
fn test_index_and_index_mut() {
133+
let mut qn = QualifiedName::new(vec!["a", "b", "c"]);
134+
assert_eq!(qn[0], "a");
135+
assert_eq!(qn[2], "c");
136+
qn[1] = "NEW_ITEM".to_string();
137+
assert_eq!(qn.to_string(), "a::NEW_ITEM::c");
138+
}
139+
140+
#[test]
141+
#[should_panic]
142+
fn test_index_out_of_bounds() {
143+
let qn = QualifiedName::new(vec!["a"]);
144+
// This should panic
145+
let _ = qn[1];
146+
}
147+
148+
#[test]
149+
fn test_insert() {
150+
let mut qn = QualifiedName::new(vec!["start", "end"]);
151+
152+
qn.insert(1, "middle".to_string());
153+
assert_eq!(qn.to_string(), "start::middle::end");
154+
155+
qn.insert(0, "HEAD".to_string());
156+
assert_eq!(qn.to_string(), "HEAD::start::middle::end");
157+
158+
qn.insert(4, "TAIL".to_string());
159+
assert_eq!(qn.to_string(), "HEAD::start::middle::end::TAIL");
160+
161+
let initial_len = qn.len();
162+
qn.insert(initial_len + 5, "NOPE".to_string());
163+
assert_eq!(qn.len(), initial_len);
164+
}
165+
166+
#[test]
167+
fn test_into_and_from_raw() {
168+
let original_qn = QualifiedName::new(["std", "vector"]);
169+
assert_eq!(original_qn.to_string(), "std::vector");
170+
171+
let raw_qn = QualifiedName::into_raw(original_qn);
172+
assert_eq!(raw_qn.nameCount, 2);
173+
assert!(!raw_qn.join.is_null());
174+
175+
let restored_qn = QualifiedName::from_owned_raw(raw_qn);
176+
assert_eq!(restored_qn.len(), 2);
177+
assert_eq!(restored_qn.to_string(), "std::vector");
178+
}
179+
180+
#[test]
181+
fn test_raw_freeing() {
182+
let qn = QualifiedName::new(["std", "vector"]);
183+
let raw_qn = QualifiedName::into_raw(qn);
184+
assert!(!raw_qn.join.is_null());
185+
assert!(!raw_qn.name.is_null());
186+
QualifiedName::free_raw(raw_qn);
187+
}

0 commit comments

Comments
 (0)