diff --git a/src/config_editor.rs b/src/config_editor.rs index ca4d3e5..d20330f 100644 --- a/src/config_editor.rs +++ b/src/config_editor.rs @@ -94,16 +94,16 @@ pub(crate) fn fixes_for_problem(problem: &Problem, config: &Config) -> Vec { - if failure.output.sandbox_config.kind != Some(SandboxKind::Disabled) { - let perm_sel = PermSel::for_build_script(failure.crate_sel.pkg_name()); - if !failure.output.sandbox_config.allow_network.unwrap_or(false) { - edits.push(Box::new(SandboxAllowNetwork { - perm_sel: perm_sel.clone(), - })); - } - edits.push(Box::new(DisableSandbox { perm_sel })); + Problem::ExecutionFailed(failure) + if failure.output.sandbox_config.kind != Some(SandboxKind::Disabled) => + { + let perm_sel = PermSel::for_build_script(failure.crate_sel.pkg_name()); + if !failure.output.sandbox_config.allow_network.unwrap_or(false) { + edits.push(Box::new(SandboxAllowNetwork { + perm_sel: perm_sel.clone(), + })); } + edits.push(Box::new(DisableSandbox { perm_sel })); } Problem::DisallowedBuildInstruction(failure) => { edits.append(&mut edits_for_build_instruction(failure)); diff --git a/src/crate_index.rs b/src/crate_index.rs index 5f1a11d..151ec54 100644 --- a/src/crate_index.rs +++ b/src/crate_index.rs @@ -201,11 +201,7 @@ impl CrateIndex { if let Some(pkg_id) = self.dir_to_pkg_id.get(path) { return Some(pkg_id); } - if let Some(parent) = path.parent() { - path = parent; - } else { - return None; - } + path = path.parent()?; } } diff --git a/src/demangle.rs b/src/demangle.rs index f4694ea..5ea92c0 100644 --- a/src/demangle.rs +++ b/src/demangle.rs @@ -17,9 +17,15 @@ pub(crate) enum DemangleToken<'data> { } #[derive(Copy, Clone, Debug)] -pub(crate) struct DemangleIterator<'data> { - outer: &'data str, - inner: Option<&'data str>, +pub(crate) enum DemangleIterator<'data> { + V0 { + remaining: &'data str, + }, + Legacy { + outer: &'data str, + inner: Option<&'data str>, + }, + Empty, } #[derive(Copy, Clone, Debug)] @@ -31,16 +37,19 @@ pub(crate) struct NonMangledIterator<'data> { /// An iterator that processes a mangled string and provides demangled tokens. impl<'data> DemangleIterator<'data> { pub(crate) fn new(data: &'data str) -> Self { + // Check for V0 mangling (_R...) + if let Some(rest) = data.strip_prefix("_R") { + return Self::V0 { remaining: rest }; + } + + // Check for legacy mangling (_ZN...E) if let Some(rest) = data.strip_prefix("_ZN").and_then(|d| d.strip_suffix('E')) { - Self { + Self::Legacy { outer: rest, inner: None, } } else { - Self { - outer: "", - inner: None, - } + Self::Empty } } } @@ -72,56 +81,363 @@ fn symbol(esc: &str) -> Result { } } +// V0 mangling helpers +fn is_base62_digit(b: u8) -> bool { + b.is_ascii_alphanumeric() +} + +fn parse_decimal(data: &str) -> Option<(u64, &str)> { + let mut value = 0u64; + let mut len = 0; + for &b in data.as_bytes() { + if b.is_ascii_digit() { + value = value.checked_mul(10)?.checked_add((b - b'0') as u64)?; + len += 1; + } else { + break; + } + } + if len == 0 { + None + } else { + Some((value, &data[len..])) + } +} + +fn parse_disambiguator(data: &str) -> Option<&str> { + if let Some(rest) = data.strip_prefix('s') { + // Disambiguator - skip 's' and everything until '_' + if let Some(end) = rest.find('_') { + return Some(&rest[end + 1..]); // Skip past the '_' + } + None + } else { + Some(data) + } +} + +fn parse_undisambiguated_identifier(data: &str) -> Option<(&str, &str)> { + // Check for punycode (u followed by decimal length) + if let Some(rest) = data.strip_prefix('u') { + if let Some((len, rest)) = parse_decimal(rest) { + if let Some(rest) = rest.strip_prefix('_') { + // Punycode identifier - for simplicity, we'll just extract the raw bytes + if len as usize <= rest.len() { + let (ident, rest) = rest.split_at(len as usize); + return Some((ident, rest)); + } + } + } + return None; + } + + // Regular identifier: decimal length followed by that many bytes + let (len, rest) = parse_decimal(data)?; + if len as usize <= rest.len() { + let (ident, rest) = rest.split_at(len as usize); + Some((ident, rest)) + } else { + None + } +} + +fn parse_identifier(data: &str) -> Option<(&str, &str)> { + // First handle the optional disambiguator + let data_after_disambiguator = parse_disambiguator(data)?; + // Then parse the actual identifier + parse_undisambiguated_identifier(data_after_disambiguator) +} + impl<'data> Iterator for DemangleIterator<'data> { type Item = DemangleToken<'data>; fn next(&mut self) -> Option { - if self.inner == Some("") { - self.inner = None; - } - if let Some(data) = self.inner.as_mut() { - while let Some(rest) = data.strip_prefix('.') { - *data = rest; + match self { + DemangleIterator::V0 { remaining } => { + if remaining.is_empty() { + return None; + } + + let tag = remaining.as_bytes()[0]; + let rest = &remaining[1..]; + + macro_rules! set_remaining { + ($val:expr) => { + *remaining = $val + }; + } + + match tag { + b'C' => { + // Crate root - followed by identifier + if let Some((ident, new_rest)) = parse_identifier(rest) { + set_remaining!(new_rest); + return Some(DemangleToken::Text(ident)); + } + } + b'N' => { + // Namespace tag - indicates we're in a path + // The next character tells us what kind of item + set_remaining!(rest); + if !rest.is_empty() { + let next_tag = rest.as_bytes()[0]; + match next_tag { + b't' => { + // Type in namespace + set_remaining!(&rest[1..]); + return self.next(); + } + b'v' => { + // Value (function/static) in namespace + set_remaining!(&rest[1..]); + return self.next(); + } + b'C' => { + // Crate root in path + return self.next(); + } + _ => { + // Try to parse as identifier directly + if let Some((ident, new_rest)) = parse_identifier(rest) { + set_remaining!(new_rest); + return Some(DemangleToken::Text(ident)); + } + } + } + } + return self.next(); + } + b'v' | b't' | b'm' => { + // Value/type/module item - followed by path + set_remaining!(rest); + return self.next(); + } + b'M' => { + // Inherent impl + set_remaining!(rest); + if let Some((_, new_rest)) = skip_v0_type(rest) { + set_remaining!(new_rest); + } + return self.next(); + } + b'X' => { + // Trait impl: X [] + // Skip the optional disambiguator, then parse the impl-path naturally. + // The impl-path contains the crate/module info we need for API detection. + set_remaining!(parse_disambiguator(rest).unwrap_or(rest)); + return self.next(); + } + b'Y' => { + // : Y (no disambiguator) + set_remaining!(rest); + return self.next(); + } + b'B' => { + // Back-reference: B_ - skip it as we don't resolve back-refs + if let Some(pos) = rest.find('_') { + set_remaining!(&rest[pos + 1..]); + } else { + set_remaining!(rest); + } + return self.next(); + } + b'I' => { + // Generic args start + set_remaining!(rest); + return Some(DemangleToken::Char('<')); + } + b'E' => { + // End marker (generic args or other) + set_remaining!(rest); + return Some(DemangleToken::Char('>')); + } + b'p' => { + // Pointer + set_remaining!(rest); + return Some(DemangleToken::Char('*')); + } + b'R' | b'Q' => { + // Reference + set_remaining!(rest); + return Some(DemangleToken::Char('&')); + } + b'A' | b'S' => { + // Array or Slice + set_remaining!(rest); + if tag == b'A' { + return Some(DemangleToken::Char('[')); + } + return self.next(); + } + b'T' => { + // Tuple + set_remaining!(rest); + return Some(DemangleToken::Char('(')); + } + b'e' => { + // Empty tuple + set_remaining!(rest); + return Some(DemangleToken::Text("()")); + } + b'u' => { + // Unsigned integer + set_remaining!(rest); + return Some(DemangleToken::Text("u")); + } + b'i' => { + // Signed integer + set_remaining!(rest); + return Some(DemangleToken::Text("i")); + } + b'_' => { + // Separator in generic args + set_remaining!(rest); + return Some(DemangleToken::Char(',')); + } + _ if is_base62_digit(tag) => { + // This is an identifier (starts with digit or letter) + if let Some((ident, new_rest)) = parse_identifier(remaining) { + set_remaining!(new_rest); + return Some(DemangleToken::Text(ident)); + } + } + _ => { + // Unknown tag - skip it + set_remaining!(rest); + return self.next(); + } + } + + // If we couldn't parse, stop iteration + None } - if let Some(rest) = data.strip_prefix('$') { - if let Some(end_escape) = rest.find('$') { - *data = &rest[end_escape + 1..]; - if let Ok(ch) = symbol(&rest[..end_escape]) { - return Some(DemangleToken::Char(ch)); + DemangleIterator::Legacy { outer, inner } => { + // Legacy mangling + if *inner == Some("") { + *inner = None; + } + if let Some(data) = inner.as_mut() { + while let Some(rest) = data.strip_prefix('.') { + *data = rest; + } + if let Some(rest) = data.strip_prefix('$') { + if let Some(end_escape) = rest.find('$') { + *data = &rest[end_escape + 1..]; + if let Ok(ch) = symbol(&rest[..end_escape]) { + return Some(DemangleToken::Char(ch)); + } + return Some(DemangleToken::UnsupportedEscape(&rest[..end_escape])); + } } - return Some(DemangleToken::UnsupportedEscape(&rest[..end_escape])); + let end = data + .bytes() + .position(|b| b == b'.' || b == b'$') + .unwrap_or(data.len()); + let text = &data[..end]; + *data = &data[end..]; + return Some(DemangleToken::Text(text)); } + if outer.is_empty() { + return None; + } + let num_digits = outer.bytes().position(|byte| !byte.is_ascii_digit())?; + let (length_str, rest) = outer.split_at(num_digits); + let length = length_str.parse().ok()?; + if length > rest.len() { + return None; + } + let (part, rest) = rest.split_at(length); + *outer = rest; + if let Some(rest) = part.strip_prefix('_') { + *inner = Some(rest); + return self.next(); + } + if part.contains('$') { + *inner = Some(part); + return self.next(); + } + + Some(DemangleToken::Text(part)) } - let end = data - .bytes() - .position(|b| b == b'.' || b == b'$') - .unwrap_or(data.len()); - let text = &data[..end]; - *data = &data[end..]; - return Some(DemangleToken::Text(text)); + DemangleIterator::Empty => None, } - let data = &mut self.outer; - if data.is_empty() { - return None; + } +} + +// Helper to skip over a V0 type during parsing +fn skip_v0_type(data: &str) -> Option<((), &str)> { + if data.is_empty() { + return None; + } + + let tag = data.as_bytes()[0]; + let rest = &data[1..]; + + match tag { + b'C' => { + // Crate-relative path + parse_identifier(rest).map(|(_, r)| ((), r)) } - let num_digits = data.bytes().position(|byte| !byte.is_ascii_digit())?; - let (length_str, rest) = data.split_at(num_digits); - let length = length_str.parse().ok()?; - if length > rest.len() { - return None; + b'N' => { + // Nested path - recursively skip + skip_v0_type(rest) } - let (part, rest) = rest.split_at(length); - *data = rest; - if let Some(rest) = part.strip_prefix('_') { - self.inner = Some(rest); - return self.next(); + b'p' | b'R' | b'Q' => { + // Pointer/reference - skip the pointee type + skip_v0_type(rest) } - if part.contains('$') { - self.inner = Some(part); - return self.next(); + b'A' | b'S' => { + // Array/slice - skip the element type + skip_v0_type(rest) + } + b'T' => { + // Tuple - skip until we find the end + let mut current = rest; + loop { + if current.is_empty() { + return None; + } + if current.as_bytes()[0] == b'E' { + return Some(((), ¤t[1..])); + } + if let Some((_, new_rest)) = skip_v0_type(current) { + current = new_rest; + } else { + return None; + } + } + } + b'I' => { + // Generic args - skip the path and all args + if let Some((_, mut current)) = skip_v0_type(rest) { + loop { + if current.is_empty() { + return None; + } + if current.as_bytes()[0] == b'E' { + return Some(((), ¤t[1..])); + } + if let Some((_, new_rest)) = skip_v0_type(current) { + current = new_rest; + } else { + return None; + } + } + } + None + } + b'e' | b'u' | b'i' | b'f' | b'b' | b'c' | b'z' | b'v' => { + // Primitive types + Some(((), rest)) + } + _ if is_base62_digit(tag) => { + // Identifier + parse_identifier(data).map(|(_, r)| ((), r)) + } + _ => { + // Unknown - try to skip + Some(((), rest)) } - - Some(DemangleToken::Text(part)) } } @@ -335,4 +651,95 @@ mod tests { ], ); } + + #[test] + fn test_v0_simple() { + // Real V0 mangled symbol from rustc + // Test without rustc-demangle comparison since V0 output format may differ + let mangled = "_RNvCscaAa4Ty0KMw_6simple11hello_world"; + let actual: Vec = DemangleIterator::new(mangled) + .map(|token| match token { + DemangleToken::Text(text) => text.to_owned(), + DemangleToken::Char(ch) => ch.to_string(), + DemangleToken::UnsupportedEscape(esc) => esc.to_owned(), + }) + .collect(); + // No "::" separator tokens - identifiers are returned directly like the legacy format + assert_eq!(actual, &["simple", "hello_world"]); + } + + #[test] + fn test_v0_with_generics() { + // V0 symbol with generic instantiation (simplified version) + let mangled = "_RINvCs0_5crate8functionpE"; + let actual: Vec = DemangleIterator::new(mangled) + .map(|token| match token { + DemangleToken::Text(text) => text.to_owned(), + DemangleToken::Char(ch) => ch.to_string(), + DemangleToken::UnsupportedEscape(esc) => esc.to_owned(), + }) + .collect(); + // Should have: crate, function, <, *, > (no "::" separators) + assert!(actual.contains(&"crate".to_string())); + assert!(actual.contains(&"function".to_string())); + assert!(actual.contains(&"<".to_string())); + } + + #[test] + fn test_v0_trait_impl() { + // Real V0 symbol: <(&str, u16) as std::net::socket_addr::ToSocketAddrs>::to_socket_addrs + let mangled = "_RNvXs4_NtNtCsjrHSEGnQ3l9_3std3net11socket_addrTRetENtB5_13ToSocketAddrs15to_socket_addrs"; + let tokens: Vec = DemangleIterator::new(mangled) + .map(|token| match token { + DemangleToken::Text(text) => text.to_owned(), + DemangleToken::Char(ch) => ch.to_string(), + DemangleToken::UnsupportedEscape(esc) => esc.to_owned(), + }) + .collect(); + // The first two text tokens must be "std" and "net" for module_name() to work + let text_tokens: Vec<&str> = tokens + .iter() + .filter_map(|t| { + if t.len() > 1 || t.chars().next().is_some_and(|c| c.is_alphabetic()) { + Some(t.as_str()) + } else { + None + } + }) + .collect(); + assert_eq!( + text_tokens.first(), + Some(&"std"), + "first token must be 'std'" + ); + assert_eq!( + text_tokens.get(1), + Some(&"net"), + "second token must be 'net'" + ); + + // Also verify module_name() works + use super::*; + use crate::symbol::Symbol; + let sym = Symbol::borrowed(mangled.as_bytes()); + assert_eq!(sym.module_name(), Some("net")); + assert_eq!(sym.crate_name(), Some("std")); + } + + #[test] + fn test_v0_nested_path() { + // Test nested module paths + let mangled = "_RNvNtNtCs0_4core3ptr8non_null7cleanupE"; + let actual: Vec = DemangleIterator::new(mangled) + .map(|token| match token { + DemangleToken::Text(text) => text.to_owned(), + DemangleToken::Char(ch) => ch.to_string(), + DemangleToken::UnsupportedEscape(esc) => esc.to_owned(), + }) + .collect(); + // Should contain the path components without "::" separators + assert!(actual.contains(&"core".to_string())); + assert!(actual.contains(&"ptr".to_string())); + assert!(!actual.contains(&"::".to_string())); + } } diff --git a/src/names.rs b/src/names.rs index 0708e81..3930a1f 100644 --- a/src/names.rs +++ b/src/names.rs @@ -308,24 +308,20 @@ impl<'data, I: Clone + Iterator>> Iterator NamesIteratorState::AsSkip { gt_depth, return_point, - } => { - if *gt_depth == 0 { - match self.it.next() { - Some(DemangleToken::Text(text)) => { - self.it = return_point.clone(); - self.as_final = Some(text); - self.state = NamesIteratorState::OutputtingName; - return Some(NameToken::Part(text)); - } - _ => { - self.it = return_point.clone(); - self.as_final = None; - self.state = NamesIteratorState::Inactive; - return Some(NameToken::EndName); - } - } + } if *gt_depth == 0 => match self.it.next() { + Some(DemangleToken::Text(text)) => { + self.it = return_point.clone(); + self.as_final = Some(text); + self.state = NamesIteratorState::OutputtingName; + return Some(NameToken::Part(text)); } - } + _ => { + self.it = return_point.clone(); + self.as_final = None; + self.state = NamesIteratorState::Inactive; + return Some(NameToken::EndName); + } + }, _ => {} } } diff --git a/src/proxy/cargo.rs b/src/proxy/cargo.rs index 5406c8f..8b33300 100644 --- a/src/proxy/cargo.rs +++ b/src/proxy/cargo.rs @@ -69,14 +69,6 @@ pub(crate) fn command( .arg("--config") .arg(format!("profile.{DEFAULT_PROFILE_NAME}.incremental=false")); - // Our demangler unfortunately doesn't yet support the v0 mangling scheme. - if is_nightly() { - command.env( - "RUSTFLAGS", - "-Csymbol-mangling-version=legacy -Zunstable-options", - ); - } - // We don't currently support split debug info. command.arg("--config").arg("split-debuginfo=\"off\""); @@ -86,11 +78,3 @@ pub(crate) fn command( command.args(extra_args); command } - -/// Returns whether we're using nightly rustc. -fn is_nightly() -> bool { - Command::new("rustc") - .arg("--version") - .output() - .is_ok_and(|output| String::from_utf8_lossy(&output.stdout).contains("-nightly")) -} diff --git a/src/symbol_graph.rs b/src/symbol_graph.rs index 9ed466b..acbbf43 100644 --- a/src/symbol_graph.rs +++ b/src/symbol_graph.rs @@ -751,17 +751,20 @@ impl BinInfo<'_> { )?; } } - } else if let Some(symbol) = symbol_and_name.symbol.as_ref() { - let mut symbol_it = symbol.names()?; - while let Some((parts, name)) = symbol_it.next_name()? { - let apis = checker.apis_for_name_iterator(parts); - if !apis.is_empty() { - got_apis = true; - (callback)( - name.create_name()?, - NameSource::Symbol(symbol.clone()), - apis, - )?; + } + if !got_apis { + if let Some(symbol) = symbol_and_name.symbol.as_ref() { + let mut symbol_it = symbol.names()?; + while let Some((parts, name)) = symbol_it.next_name()? { + let apis = checker.apis_for_name_iterator(parts); + if !apis.is_empty() { + got_apis = true; + (callback)( + name.create_name()?, + NameSource::Symbol(symbol.clone()), + apis, + )?; + } } } } diff --git a/src/ui/full_term/problems_ui.rs b/src/ui/full_term/problems_ui.rs index af86a86..a0f3f4d 100644 --- a/src/ui/full_term/problems_ui.rs +++ b/src/ui/full_term/problems_ui.rs @@ -144,6 +144,9 @@ impl ProblemsUi { } } + // clippy::collapsible_match false positive: using `self.modes.len()` as a match guard + // doesn't compile due to the mutable borrow from `self.modes.last_mut()` above. + #[allow(clippy::collapsible_match)] pub(super) fn handle_key(&mut self, key: KeyEvent) -> Result<()> { let Some(mode) = self.modes.last_mut() else { return Ok(()); @@ -261,10 +264,8 @@ impl ProblemsUi { self.comment.as_deref().unwrap_or_default().into(), )); } - (Mode::SelectProblem, KeyCode::Char('a')) => { - if !self.accept_single_enabled { - self.modes.push(Mode::PromptAutoAccept); - } + (Mode::SelectProblem, KeyCode::Char('a')) if !self.accept_single_enabled => { + self.modes.push(Mode::PromptAutoAccept); } (Mode::PromptAutoAccept, KeyCode::Enter) => { self.accept_single_enabled = true;