diff --git a/objdiff-core/src/arch/mod.rs b/objdiff-core/src/arch/mod.rs index c12fd751..592ed209 100644 --- a/objdiff-core/src/arch/mod.rs +++ b/objdiff-core/src/arch/mod.rs @@ -83,7 +83,7 @@ impl fmt::Display for DataType { impl DataType { pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec { let mut strs = Vec::new(); - for (literal, label_override) in self.display_literals(endian, bytes) { + for (literal, label_override, _) in self.display_literals(endian, bytes) { let label = label_override.unwrap_or_else(|| self.to_string()); strs.push(format!("{label}: {literal:?}")) } @@ -94,7 +94,7 @@ impl DataType { &self, endian: object::Endianness, bytes: &[u8], - ) -> Vec<(String, Option)> { + ) -> Vec<(String, Option, Option)> { let mut strs = Vec::new(); if self.required_len().is_some_and(|l| bytes.len() < l) { log::warn!( @@ -118,34 +118,34 @@ impl DataType { match self { DataType::Int8 => { let i = i8::from_ne_bytes(bytes.try_into().unwrap()); - strs.push((format!("{i:#x}"), None)); + strs.push((format!("{i:#x}"), None, None)); if i < 0 { - strs.push((format!("{:#x}", ReallySigned(i)), None)); + strs.push((format!("{:#x}", ReallySigned(i)), None, None)); } } DataType::Int16 => { let i = endian.read_i16_bytes(bytes.try_into().unwrap()); - strs.push((format!("{i:#x}"), None)); + strs.push((format!("{i:#x}"), None, None)); if i < 0 { - strs.push((format!("{:#x}", ReallySigned(i)), None)); + strs.push((format!("{:#x}", ReallySigned(i)), None, None)); } } DataType::Int32 => { let i = endian.read_i32_bytes(bytes.try_into().unwrap()); - strs.push((format!("{i:#x}"), None)); + strs.push((format!("{i:#x}"), None, None)); if i < 0 { - strs.push((format!("{:#x}", ReallySigned(i)), None)); + strs.push((format!("{:#x}", ReallySigned(i)), None, None)); } } DataType::Int64 => { let i = endian.read_i64_bytes(bytes.try_into().unwrap()); - strs.push((format!("{i:#x}"), None)); + strs.push((format!("{i:#x}"), None, None)); if i < 0 { - strs.push((format!("{:#x}", ReallySigned(i)), None)); + strs.push((format!("{:#x}", ReallySigned(i)), None, None)); } } DataType::Float => { @@ -156,6 +156,7 @@ impl DataType { object::Endianness::Big => f32::from_be_bytes(bytes), }), None, + None, )); } DataType::Double => { @@ -166,10 +167,11 @@ impl DataType { object::Endianness::Big => f64::from_be_bytes(bytes), }), None, + None, )); } DataType::Bytes => { - strs.push((format!("{bytes:#?}"), None)); + strs.push((format!("{bytes:#?}"), None, None)); } DataType::String => { if let Some(nul_idx) = bytes.iter().position(|&c| c == b'\0') { @@ -177,13 +179,17 @@ impl DataType { // Special case to display (ASCII) as the label for ASCII-only strings. let (cow, _, had_errors) = encoding_rs::UTF_8.decode(str_bytes); if !had_errors && cow.is_ascii() { - strs.push((format!("{cow}"), Some("ASCII".into()))); + let string = format!("{cow}"); + let copy_string = escape_special_ascii_characters(string.clone()); + strs.push((string, Some("ASCII".into()), Some(copy_string))); } for (encoding, encoding_name) in SUPPORTED_ENCODINGS { let (cow, _, had_errors) = encoding.decode(str_bytes); // Avoid showing ASCII-only strings more than once if the encoding is ASCII-compatible. if !had_errors && (!encoding.is_ascii_compatible() || !cow.is_ascii()) { - strs.push((format!("{cow}"), Some(encoding_name.into()))); + let string = format!("{cow}"); + let copy_string = escape_special_ascii_characters(string.clone()); + strs.push((string, Some(encoding_name.into()), Some(copy_string))); } } } @@ -499,3 +505,21 @@ pub struct RelocationOverride { pub target: RelocationOverrideTarget, pub addend: i64, } + +/// Escape ASCII characters such as \n or \t, but not Unicode characters such as \u{3000}. +/// Suitable for copying to clipboard. +fn escape_special_ascii_characters(value: String) -> String { + let mut escaped = String::new(); + escaped.push('"'); + for c in value.chars() { + if c.is_ascii() { + for e in c.escape_default() { + escaped.push(e); + } + } else { + escaped.push(c); + } + } + escaped.push('"'); + escaped +} diff --git a/objdiff-core/src/arch/ppc/mod.rs b/objdiff-core/src/arch/ppc/mod.rs index 24f01eb0..c61ea362 100644 --- a/objdiff-core/src/arch/ppc/mod.rs +++ b/objdiff-core/src/arch/ppc/mod.rs @@ -440,9 +440,13 @@ impl Arch for ArchPpc { let simplified = ins.simplified().to_string(); let show_orig = orig != simplified; let mut out = Vec::with_capacity(2); - out.push(ContextItem::Copy { value: simplified, label: None }); + out.push(ContextItem::Copy { value: simplified, label: None, copy_string: None }); if show_orig { - out.push(ContextItem::Copy { value: orig, label: Some("original".to_string()) }); + out.push(ContextItem::Copy { + value: orig, + label: Some("original".to_string()), + copy_string: None, + }); } out } diff --git a/objdiff-core/src/diff/display.rs b/objdiff-core/src/diff/display.rs index c1ec85a6..17416969 100644 --- a/objdiff-core/src/diff/display.rs +++ b/objdiff-core/src/diff/display.rs @@ -366,7 +366,7 @@ impl From<&DiffText<'_>> for HighlightKind { } pub enum ContextItem { - Copy { value: String, label: Option }, + Copy { value: String, label: Option, copy_string: Option }, Navigate { label: String, symbol_index: usize, kind: SymbolNavigationKind }, Separator, } @@ -398,9 +398,9 @@ pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec { return Vec::new(); }; let mut out = Vec::new(); - out.push(ContextItem::Copy { value: symbol.name.clone(), label: None }); + out.push(ContextItem::Copy { value: symbol.name.clone(), label: None, copy_string: None }); if let Some(name) = &symbol.demangled_name { - out.push(ContextItem::Copy { value: name.clone(), label: None }); + out.push(ContextItem::Copy { value: name.clone(), label: None, copy_string: None }); } if symbol.section.is_some() && let Some(address) = symbol.virtual_address @@ -408,6 +408,7 @@ pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec { out.push(ContextItem::Copy { value: format!("{address:x}"), label: Some("virtual address".to_string()), + copy_string: None, }); } out.append(&mut obj.arch.symbol_context(obj, symbol_index)); @@ -501,8 +502,8 @@ pub fn relocation_context( let literals = display_ins_data_literals(obj, ins); if !literals.is_empty() { out.push(ContextItem::Separator); - for (literal, label_override) in literals { - out.push(ContextItem::Copy { value: literal, label: label_override }); + for (literal, label_override, copy_string) in literals { + out.push(ContextItem::Copy { value: literal, label: label_override, copy_string }); } } } @@ -598,24 +599,37 @@ pub fn instruction_context( for byte in resolved.code { hex_string.push_str(&format!("{byte:02x}")); } - out.push(ContextItem::Copy { value: hex_string, label: Some("instruction bytes".to_string()) }); + out.push(ContextItem::Copy { + value: hex_string, + label: Some("instruction bytes".to_string()), + copy_string: None, + }); out.append(&mut obj.arch.instruction_context(obj, resolved)); if let Some(virtual_address) = resolved.symbol.virtual_address { let offset = resolved.ins_ref.address - resolved.symbol.address; out.push(ContextItem::Copy { value: format!("{:x}", virtual_address + offset), label: Some("virtual address".to_string()), + copy_string: None, }); } for arg in &ins.args { if let InstructionArg::Value(arg) = arg { - out.push(ContextItem::Copy { value: arg.to_string(), label: None }); + out.push(ContextItem::Copy { value: arg.to_string(), label: None, copy_string: None }); match arg { InstructionArgValue::Signed(v) => { - out.push(ContextItem::Copy { value: v.to_string(), label: None }); + out.push(ContextItem::Copy { + value: v.to_string(), + label: None, + copy_string: None, + }); } InstructionArgValue::Unsigned(v) => { - out.push(ContextItem::Copy { value: v.to_string(), label: None }); + out.push(ContextItem::Copy { + value: v.to_string(), + label: None, + copy_string: None, + }); } _ => {} } @@ -677,7 +691,7 @@ pub fn instruction_hover( let literals = display_ins_data_literals(obj, resolved); if !literals.is_empty() { out.push(HoverItem::Separator); - for (literal, label_override) in literals { + for (literal, label_override, _) in literals { out.push(HoverItem::Text { label: label_override.unwrap_or_else(|| ty.to_string()), value: format!("{literal:?}"), @@ -871,7 +885,7 @@ pub fn display_ins_data_labels(obj: &Object, resolved: ResolvedInstructionRef) - pub fn display_ins_data_literals( obj: &Object, resolved: ResolvedInstructionRef, -) -> Vec<(String, Option)> { +) -> Vec<(String, Option, Option)> { let Some(reloc) = resolved.relocation else { return Vec::new(); }; diff --git a/objdiff-gui/src/views/diff.rs b/objdiff-gui/src/views/diff.rs index f1ce8efc..f7c4a678 100644 --- a/objdiff-gui/src/views/diff.rs +++ b/objdiff-gui/src/views/diff.rs @@ -865,24 +865,6 @@ pub fn hover_items_ui(ui: &mut Ui, items: Vec, appearance: &Appearanc } } -/// Escape ASCII characters such as \n or \t, but not Unicode characters such as \u{3000}. -/// Suitable for copying to clipboard. -fn escape_special_ascii_characters(value: String) -> String { - let mut escaped = String::new(); - escaped.push('"'); - for c in value.chars() { - if c.is_ascii() { - for e in c.escape_default() { - escaped.push(e); - } - } else { - escaped.push(c); - } - } - escaped.push('"'); - escaped -} - pub fn context_menu_items_ui( ui: &mut Ui, items: Vec, @@ -892,7 +874,7 @@ pub fn context_menu_items_ui( let mut ret = None; for item in items { match item { - ContextItem::Copy { value, label } => { + ContextItem::Copy { value, label, copy_string } => { let mut job = LayoutJob::default(); write_text("Copy ", appearance.text_color, &mut job, appearance.code_font.clone()); write_text( @@ -912,8 +894,7 @@ pub fn context_menu_items_ui( write_text(")", appearance.text_color, &mut job, appearance.code_font.clone()); } if ui.button(job).clicked() { - let escaped = escape_special_ascii_characters(value); - ui.ctx().copy_text(escaped); + ui.ctx().copy_text(copy_string.unwrap_or(value)); ui.close(); } } diff --git a/objdiff-wasm/src/api.rs b/objdiff-wasm/src/api.rs index 584391f7..e9b5498b 100644 --- a/objdiff-wasm/src/api.rs +++ b/objdiff-wasm/src/api.rs @@ -273,6 +273,7 @@ impl GuestDisplay for Component { return vec![ContextItem::Copy(ContextItemCopy { value: "Failed to resolve instruction".to_string(), label: Some("error".to_string()), + copy_string: None, })]; }; let ins = match obj.arch.process_instruction(resolved, &diff_config) { @@ -281,6 +282,7 @@ impl GuestDisplay for Component { return vec![ContextItem::Copy(ContextItemCopy { value: e.to_string(), label: Some("error".to_string()), + copy_string: None, })]; } }; @@ -707,8 +709,8 @@ impl From for HoverItemColor { impl From for ContextItem { fn from(item: diff::display::ContextItem) -> Self { match item { - diff::display::ContextItem::Copy { value, label } => { - ContextItem::Copy(ContextItemCopy { value, label }) + diff::display::ContextItem::Copy { value, label, copy_string } => { + ContextItem::Copy(ContextItemCopy { value, label, copy_string }) } diff::display::ContextItem::Navigate { label, symbol_index, kind } => { ContextItem::Navigate(ContextItemNavigate { diff --git a/objdiff-wasm/wit/objdiff.wit b/objdiff-wasm/wit/objdiff.wit index 8e8003e3..11e4cbdd 100644 --- a/objdiff-wasm/wit/objdiff.wit +++ b/objdiff-wasm/wit/objdiff.wit @@ -143,6 +143,7 @@ interface display { record context-item-copy { value: string, label: option, + copy-string: option, } record context-item-navigate {