use std::{ fs, io::{BufRead, Write}, num::ParseIntError, str::FromStr, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use cwdemangle::{demangle, DemangleOptions}; use filetime::FileTime; use once_cell::sync::Lazy; use regex::{Captures, Regex}; use tracing::{debug, info, warn}; use typed_path::Utf8NativePath; use xxhash_rust::xxh3::xxh3_64; use crate::{ analysis::cfa::SectionAddress, obj::{ ObjDataKind, ObjInfo, ObjKind, ObjSectionKind, ObjSplit, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, ObjUnit, SectionIndex, }, util::{ file::{buf_writer, FileReadInfo}, split::default_section_align, }, vfs::open_file, }; pub fn parse_u32(s: &str) -> Result { if let Some(s) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { u32::from_str_radix(s, 16) } else { s.parse::() } } pub fn parse_i32(s: &str) -> Result { if let Some(s) = s.strip_prefix("-0x").or_else(|| s.strip_prefix("-0X")) { i32::from_str_radix(s, 16).map(|v| -v) } else if let Some(s) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { i32::from_str_radix(s, 16) } else { s.parse::() } } pub fn apply_symbols_file( path: &Utf8NativePath, obj: &mut ObjInfo, ) -> Result> { Ok(if fs::metadata(path).is_ok_and(|m| m.is_file()) { let mut file = open_file(path, true)?; let cached = FileReadInfo::new(file.as_mut())?; for result in file.lines() { let line = match result { Ok(line) => line, Err(e) => bail!("Failed to process symbols file: {e:?}"), }; if let Some(symbol) = parse_symbol_line(&line, obj)? { obj.add_symbol(symbol, true)?; } } Some(cached) } else { None }) } pub fn parse_symbol_line(line: &str, obj: &mut ObjInfo) -> Result> { static SYMBOL_LINE: Lazy = Lazy::new(|| { Regex::new( "^\\s*(?P[^\\s=]+)\\s*=\\s*(?:(?P
[A-Za-z0-9.]+):)?(?P[0-9A-Fa-fXx]+);(?:\\s*//\\s*(?P.*))?$", ) .unwrap() }); static COMMENT_LINE: Lazy = Lazy::new(|| Regex::new("^\\s*(?://|#).*$").unwrap()); if let Some(captures) = SYMBOL_LINE.captures(line) { let name = captures["name"].to_string(); let addr = parse_u32(&captures["addr"])?; let section_name = captures["section"].to_string(); let section = if section_name == "ABS" { None } else if let Some((section_index, _)) = obj.sections.by_name(§ion_name)? { Some(section_index) } else if obj.kind == ObjKind::Executable { let (section_index, section) = obj.sections.at_address_mut(addr)?; if !section.section_known { section.rename(section_name)?; } Some(section_index) } else { bail!("Section {} not found", section_name) }; let demangled_name = demangle(&name, &DemangleOptions::default()); let mut symbol = ObjSymbol { name, demangled_name, address: addr as u64, section, ..Default::default() }; // TODO move somewhere common if symbol.name.starts_with("..") { symbol.flags.0 |= ObjSymbolFlags::Exported; } let attrs = captures["attrs"].split(' '); for attr in attrs { if let Some((name, value)) = attr.split_once(':') { match name { "type" => { symbol.kind = symbol_kind_from_str(value) .ok_or_else(|| anyhow!("Unknown symbol type '{}'", value))?; } "size" => { symbol.size = parse_u32(value)? as u64; symbol.size_known = true; } "scope" => { symbol.flags.0 |= symbol_flags_from_str(value) .ok_or_else(|| anyhow!("Unknown symbol scope '{}'", value))?; } "align" => { symbol.align = Some(parse_u32(value)?); } "data" => { symbol.data_kind = symbol_data_kind_from_str(value) .ok_or_else(|| anyhow!("Unknown symbol data type '{}'", value))?; } "hash" => { let hash = parse_u32(value)?; symbol.name_hash = Some(hash); if symbol.demangled_name_hash.is_none() { symbol.demangled_name_hash = Some(hash); } } "dhash" => { symbol.demangled_name_hash = Some(parse_u32(value)?); } _ => bail!("Unknown symbol attribute '{name}'"), } } else { match attr { "hidden" => { symbol.flags.0 |= ObjSymbolFlags::Hidden; } "force_active" => { symbol.flags.0 |= ObjSymbolFlags::Exported; } "stripped" => { symbol.flags.0 |= ObjSymbolFlags::Stripped; } "noreloc" => { ensure!( symbol.size != 0, "Symbol {} requires size != 0 with noreloc", symbol.name ); ensure!( section.is_some(), "Symbol {} requires section with noreloc", symbol.name ); let addr = SectionAddress::new(section.unwrap(), symbol.address as u32); obj.blocked_relocation_sources.insert(addr, addr + symbol.size as u32); symbol.flags.0 |= ObjSymbolFlags::NoReloc; } "noexport" => { symbol.flags.0 |= ObjSymbolFlags::NoExport; } _ => bail!("Unknown symbol attribute '{attr}'"), } } } Ok(Some(symbol)) } else if line.is_empty() || COMMENT_LINE.is_match(line) { Ok(None) } else { Err(anyhow!("Failed to parse symbol line '{line}'")) } } pub fn is_skip_symbol(symbol: &ObjSymbol) -> bool { if symbol.flags.is_no_write() { return true; } // symbol.name.starts_with("lbl_") // || symbol.name.starts_with("func_") // || symbol.name.starts_with("switch_") // || symbol.name.starts_with("float_") // || symbol.name.starts_with("double_") false } pub fn is_auto_symbol(symbol: &ObjSymbol) -> bool { symbol.name.starts_with("lbl_") || symbol.name.starts_with("fn_") || symbol.name.starts_with("jumptable_") || symbol.name.starts_with("gap_") || symbol.name.starts_with("pad_") } pub fn is_auto_label(symbol: &ObjSymbol) -> bool { symbol.name.starts_with("lbl_") } pub fn is_auto_jump_table(symbol: &ObjSymbol) -> bool { symbol.name.starts_with("jumptable_") } fn write_if_unchanged( path: &Utf8NativePath, cb: Cb, cached_file: Option, ) -> Result<()> where Cb: FnOnce(&mut dyn Write) -> Result<()>, { if let Some(cached_file) = cached_file { // Check file mtime let new_mtime = fs::metadata(path).ok().map(|m| FileTime::from_last_modification_time(&m)); if let (Some(new_mtime), Some(old_mtime)) = (new_mtime, cached_file.mtime) { if new_mtime != old_mtime { // File changed, don't write warn!(path = %path, "File changed since read, not updating"); return Ok(()); } } // Write to buffer and compare with hash let mut buf = Vec::new(); cb(&mut buf)?; if xxh3_64(&buf) == cached_file.hash { // No changes debug!(path = %path, "File unchanged"); return Ok(()); } // Write to file info!("Writing updated {}", path); fs::write(path, &buf)?; } else { // Write directly let mut w = buf_writer(path)?; cb(&mut w)?; w.flush()?; } Ok(()) } #[inline] pub fn write_symbols_file( path: &Utf8NativePath, obj: &ObjInfo, cached_file: Option, ) -> Result<()> { write_if_unchanged(path, |w| write_symbols(w, obj), cached_file) } pub fn write_symbols(w: &mut W, obj: &ObjInfo) -> Result<()> where W: Write + ?Sized { for (_, symbol) in obj.symbols.iter_ordered() { if symbol.kind == ObjSymbolKind::Section || is_skip_symbol(symbol) { continue; } write_symbol(w, obj, symbol)?; } Ok(()) } fn write_symbol(w: &mut W, obj: &ObjInfo, symbol: &ObjSymbol) -> Result<()> where W: Write + ?Sized { write!(w, "{} = ", symbol.name)?; let section = symbol.section.and_then(|idx| obj.sections.get(idx)); if let Some(section) = section { write!(w, "{}:", section.name)?; } write!(w, "{:#010X}; //", symbol.address)?; write!(w, " type:{}", symbol_kind_to_str(symbol.kind))?; if symbol.size_known && symbol.size > 0 { write!(w, " size:{:#X}", symbol.size)?; } if let Some(scope) = symbol_flags_to_str(symbol.flags) { write!(w, " scope:{scope}")?; } if let Some(align) = symbol.align { write!(w, " align:{align}")?; } if let Some(kind) = symbol_data_kind_to_str(symbol.data_kind) { write!(w, " data:{kind}")?; } if let Some(hash) = symbol.name_hash { write!(w, " hash:{:#010X}", hash)?; } if let Some(hash) = symbol.demangled_name_hash { if symbol.name_hash != symbol.demangled_name_hash { write!(w, " dhash:{:#010X}", hash)?; } } if symbol.flags.is_hidden() { write!(w, " hidden")?; } // if symbol.flags.is_force_active() { // write!(w, " force_active")?; // } if symbol.flags.is_stripped() { write!(w, " stripped")?; } if symbol.flags.is_no_reloc() { write!(w, " noreloc")?; } if symbol.flags.is_no_export() { write!(w, " noexport")?; } writeln!(w)?; Ok(()) } #[inline] fn symbol_kind_to_str(kind: ObjSymbolKind) -> &'static str { match kind { ObjSymbolKind::Unknown => "label", ObjSymbolKind::Function => "function", ObjSymbolKind::Object => "object", ObjSymbolKind::Section => "section", } } #[inline] fn symbol_data_kind_to_str(kind: ObjDataKind) -> Option<&'static str> { match kind { ObjDataKind::Unknown => None, ObjDataKind::Byte => Some("byte"), ObjDataKind::Byte2 => Some("2byte"), ObjDataKind::Byte4 => Some("4byte"), ObjDataKind::Byte8 => Some("8byte"), ObjDataKind::Float => Some("float"), ObjDataKind::Double => Some("double"), ObjDataKind::String => Some("string"), ObjDataKind::String16 => Some("wstring"), ObjDataKind::StringTable => Some("string_table"), ObjDataKind::String16Table => Some("wstring_table"), ObjDataKind::Int => Some("int"), ObjDataKind::Short => Some("short"), } } #[inline] fn symbol_kind_from_str(s: &str) -> Option { match s { "label" => Some(ObjSymbolKind::Unknown), "function" => Some(ObjSymbolKind::Function), "object" => Some(ObjSymbolKind::Object), "section" => Some(ObjSymbolKind::Section), _ => None, } } #[inline] fn symbol_flags_to_str(flags: ObjSymbolFlagSet) -> Option<&'static str> { if flags.0.contains(ObjSymbolFlags::Weak) { Some("weak") } else if flags.0.contains(ObjSymbolFlags::Global) { Some("global") } else if flags.0.contains(ObjSymbolFlags::Local) { Some("local") } else { None } } #[inline] fn symbol_flags_from_str(s: &str) -> Option { match s { "common" => Some(ObjSymbolFlags::Common), "weak" => Some(ObjSymbolFlags::Weak), "global" => Some(ObjSymbolFlags::Global), "local" => Some(ObjSymbolFlags::Local), _ => None, } } #[inline] fn symbol_data_kind_from_str(s: &str) -> Option { match s { "byte" => Some(ObjDataKind::Byte), "2byte" => Some(ObjDataKind::Byte2), "4byte" => Some(ObjDataKind::Byte4), "8byte" => Some(ObjDataKind::Byte8), "float" => Some(ObjDataKind::Float), "double" => Some(ObjDataKind::Double), "string" => Some(ObjDataKind::String), "wstring" => Some(ObjDataKind::String16), "string_table" => Some(ObjDataKind::StringTable), "wstring_table" => Some(ObjDataKind::String16Table), "int" => Some(ObjDataKind::Int), "short" => Some(ObjDataKind::Short), _ => None, } } #[inline] fn section_kind_from_str(s: &str) -> Option { match s { "code" | "text" => Some(ObjSectionKind::Code), "data" => Some(ObjSectionKind::Data), "rodata" => Some(ObjSectionKind::ReadOnlyData), "bss" => Some(ObjSectionKind::Bss), _ => None, } } #[inline] fn section_kind_to_str(kind: ObjSectionKind) -> &'static str { match kind { ObjSectionKind::Code => "code", ObjSectionKind::Data => "data", ObjSectionKind::ReadOnlyData => "rodata", ObjSectionKind::Bss => "bss", } } #[inline] pub fn write_splits_file( path: &Utf8NativePath, obj: &ObjInfo, all: bool, cached_file: Option, ) -> Result<()> { write_if_unchanged(path, |w| write_splits(w, obj, all), cached_file) } pub fn write_splits(w: &mut W, obj: &ObjInfo, all: bool) -> Result<()> where W: Write + ?Sized { writeln!(w, "Sections:")?; for (_, section) in obj.sections.iter() { write!(w, "\t{:<11} type:{}", section.name, section_kind_to_str(section.kind))?; if section.align > 0 { write!(w, " align:{}", section.align)?; } writeln!(w)?; } for unit in obj.link_order.iter().filter(|unit| all || !unit.autogenerated) { write!(w, "\n{}:", unit.name)?; if let Some(comment_version) = unit.comment_version { write!(w, " comment:{}", comment_version)?; } if let Some(order) = unit.order { write!(w, " order:{}", order)?; } writeln!(w)?; let mut split_iter = obj.sections.all_splits().peekable(); while let Some((_section_index, section, addr, split)) = split_iter.next() { if split.unit != unit.name { continue; } let end = if split.end > 0 { split.end } else { split_iter.peek().map(|&(_, _, addr, _)| addr).unwrap_or(0) }; write!(w, "\t{:<11} start:{:#010X} end:{:#010X}", section.name, addr, end)?; if let Some(align) = split.align { if align != default_section_align(section) as u32 { write!(w, " align:{}", align)?; } } if split.common { write!(w, " common")?; } if let Some(name) = &split.rename { write!(w, " rename:{}", name)?; } if split.skip { write!(w, " skip")?; } writeln!(w)?; } } Ok(()) } struct SplitSection { name: String, start: u32, end: u32, align: Option, /// Whether this is a part of common BSS. common: bool, rename: Option, skip: bool, } struct SplitUnit { name: String, /// MW `.comment` section version comment_version: Option, /// Influences the order of this unit relative to other ordered units. order: Option, } pub struct SectionDef { pub name: String, pub kind: Option, pub align: Option, } enum SplitLine { Unit(SplitUnit), UnitSection(SplitSection), SectionsStart, Section(SectionDef), None, } fn parse_split_line(line: &str, state: &SplitState) -> Result { static UNIT_LINE: Lazy = Lazy::new(|| Regex::new("^\\s*(?P[^\\s:]+)\\s*:\\s*(?P.*)$").unwrap()); static SECTION_LINE: Lazy = Lazy::new(|| Regex::new("^\\s*(?P\\S+)\\s*(?P.*)$").unwrap()); static COMMENT_LINE: Lazy = Lazy::new(|| Regex::new("^\\s*(?://|#).*$").unwrap()); if line.is_empty() || COMMENT_LINE.is_match(line) { Ok(SplitLine::None) } else if let Some(captures) = UNIT_LINE.captures(line) { parse_unit_line(captures).with_context(|| format!("While parsing split line: '{line}'")) } else if let Some(captures) = SECTION_LINE.captures(line) { parse_section_line(captures, state) .with_context(|| format!("While parsing split line: '{line}'")) } else { Err(anyhow!("Failed to parse split line: '{line}'")) } } fn parse_unit_line(captures: Captures) -> Result { let name = &captures["name"]; if name == "Sections" { return Ok(SplitLine::SectionsStart); } let mut unit = SplitUnit { name: name.to_string(), comment_version: None, order: None }; for attr in captures["attrs"].split(' ').filter(|&s| !s.is_empty()) { if let Some((attr, value)) = attr.split_once(':') { match attr { "comment" => unit.comment_version = Some(u8::from_str(value)?), "order" => unit.order = Some(parse_i32(value)?), _ => bail!("Unknown unit attribute '{}'", attr), } } else { bail!("Unknown unit attribute '{attr}'"); } } Ok(SplitLine::Unit(unit)) } fn parse_section_line(captures: Captures, state: &SplitState) -> Result { if matches!(state, SplitState::Sections(_)) { let name = &captures["name"]; let mut section = SectionDef { name: name.to_string(), kind: None, align: None }; for attr in captures["attrs"].split(' ').filter(|&s| !s.is_empty()) { if let Some((attr, value)) = attr.split_once(':') { match attr { "type" => { section.kind = Some( section_kind_from_str(value) .ok_or_else(|| anyhow!("Unknown section type '{}'", value))?, ); } "align" => { section.align = Some(parse_u32(value)?); } _ => bail!("Unknown section attribute '{attr}'"), } } else { bail!("Unknown section attribute '{attr}'"); } } return Ok(SplitLine::Section(section)); } let mut start = None; let mut end = None; let mut section = SplitSection { name: captures["name"].to_string(), start: 0, end: 0, align: None, common: false, rename: None, skip: false, }; for attr in captures["attrs"].split(' ').filter(|&s| !s.is_empty()) { if let Some((attr, value)) = attr.split_once(':') { match attr { "start" => start = Some(parse_u32(value)?), "end" => end = Some(parse_u32(value)?), "align" => section.align = Some(parse_u32(value)?), "rename" => section.rename = Some(value.to_string()), _ => bail!("Unknown split attribute '{attr}'"), } } else { match attr { "common" => { section.common = true; if section.align.is_none() { section.align = Some(4); } } "skip" => section.skip = true, _ => bail!("Unknown split attribute '{attr}'"), } } } if let (Some(start), Some(end)) = (start, end) { section.start = start; section.end = end; Ok(SplitLine::UnitSection(section)) } else { Err(anyhow!("Section '{}' missing start or end address", section.name)) } } enum SplitState { None, Sections(SectionIndex), Unit(String), } pub fn apply_splits_file(path: &Utf8NativePath, obj: &mut ObjInfo) -> Result> { Ok(if fs::metadata(path).is_ok_and(|m| m.is_file()) { let mut file = open_file(path, true)?; let cached = FileReadInfo::new(file.as_mut())?; apply_splits(file.as_mut(), obj)?; Some(cached) } else { None }) } pub fn apply_splits(r: &mut R, obj: &mut ObjInfo) -> Result<()> where R: BufRead + ?Sized { let mut state = SplitState::None; for result in r.lines() { let line = match result { Ok(line) => line, Err(e) => return Err(e.into()), }; let split_line = parse_split_line(&line, &state)?; match (&mut state, split_line) { ( SplitState::None | SplitState::Unit(_) | SplitState::Sections(_), SplitLine::Unit(SplitUnit { name, comment_version, order }), ) => { obj.link_order.push(ObjUnit { name: name.clone(), autogenerated: false, comment_version, order, }); state = SplitState::Unit(name); } (SplitState::None, SplitLine::UnitSection(SplitSection { name, .. })) => { bail!("Section {} defined outside of unit", name); } (SplitState::None | SplitState::Unit(_), SplitLine::SectionsStart) => { state = SplitState::Sections(0); } (SplitState::Sections(index), SplitLine::Section(SectionDef { name, kind, align })) => { let Some(obj_section) = obj.sections.get_mut(*index) else { bail!( "Section out of bounds: {} (index {}), object has {} sections", name, index, obj.sections.len() ); }; if obj_section.rename(name.clone()).is_err() { // Manual section obj_section.kind = kind.ok_or_else(|| anyhow!("Section '{}' missing type", name))?; obj_section.name = name; obj_section.section_known = true; } if let Some(align) = align { obj_section.align = align as u64; } *index += 1; } ( SplitState::Unit(unit), SplitLine::UnitSection(SplitSection { name, start, end, align, common, rename, skip, }), ) => { ensure!(end >= start, "Invalid split range {:#X}..{:#X}", start, end); let (section_index, _) = match obj.sections.by_name(&name)? { Some(v) => Ok(v), None => { if obj.kind == ObjKind::Executable { obj.sections.with_range(start..end) } else { Err(anyhow!("Section {} not found", name)) } } }?; let section = obj.sections.get_mut(section_index).unwrap(); let section_end = (section.address + section.size) as u32; ensure!( section.contains_range(start..end) || (start == section_end && end == section_end), "Section {} ({:#010X}..{:#010X}) does not contain range {:#010X}..{:#010X}", name, section.address, section.address + section.size, start, end ); section.splits.push(start, ObjSplit { unit: unit.clone(), end, align, common, autogenerated: false, skip, rename, }); } _ => {} } } Ok(()) } pub fn read_splits_sections(path: &Utf8NativePath) -> Result>> { if !fs::metadata(path).is_ok_and(|m| m.is_file()) { return Ok(None); } let file = open_file(path, true)?; let mut sections = Vec::new(); let mut state = SplitState::None; for result in file.lines() { let line = match result { Ok(line) => line, Err(e) => return Err(e.into()), }; let split_line = parse_split_line(&line, &state)?; match (&mut state, split_line) { (SplitState::None | SplitState::Unit(_), SplitLine::SectionsStart) => { state = SplitState::Sections(0); } (SplitState::Sections(index), SplitLine::Section(def)) => { sections.push(def); *index += 1; } (SplitState::Sections(_), SplitLine::None) => { // Continue } (SplitState::Sections(_), _) => { // End of sections break; } _ => {} } } if sections.is_empty() { Ok(None) } else { Ok(Some(sections)) } } pub mod signed_hex_serde { use serde::{Deserializer, Serializer}; pub fn serialize(value: &i64, serializer: S) -> Result where S: Serializer { if *value < 0 { serializer.serialize_str(&format!("-{:#X}", -value)) } else { serializer.serialize_str(&format!("{:#X}", value)) } } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de> { struct SignedHexVisitor; impl<'de> serde::de::Visitor<'de> for SignedHexVisitor { type Value = i64; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a signed hexadecimal number") } fn visit_i64(self, v: i64) -> Result where E: serde::de::Error { Ok(v) } fn visit_u64(self, v: u64) -> Result where E: serde::de::Error { v.try_into().map_err(serde::de::Error::custom) } fn visit_str(self, value: &str) -> Result where E: serde::de::Error { if let Some(s) = value.strip_prefix("-0x").or_else(|| value.strip_prefix("-0X")) { i64::from_str_radix(s, 16).map(|v| -v).map_err(serde::de::Error::custom) } else if let Some(s) = value.strip_prefix("0x").or_else(|| value.strip_prefix("0X")) { i64::from_str_radix(s, 16).map_err(serde::de::Error::custom) } else { value.parse::().map_err(serde::de::Error::custom) } } } deserializer.deserialize_any(SignedHexVisitor) } } /// A reference to a section and address within that section. /// For executable objects, section can be omitted and the address is treated as absolute. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SectionAddressRef { pub section: Option, pub address: u32, } impl SectionAddressRef { pub fn new(section: Option, address: u32) -> Self { Self { section, address } } pub fn resolve(&self, obj: &ObjInfo) -> Result { let (section_index, section) = if let Some(section) = &self.section { obj.sections .by_name(section)? .ok_or_else(|| anyhow!("Section {} not found", section))? } else if obj.kind == ObjKind::Executable { obj.sections.at_address(self.address)? } else { bail!("Section required for relocatable object address reference: {:#X}", self.address) }; ensure!( section.contains(self.address), "Address {:#X} not in section {} ({:#X}..{:#X})", self.address, section.name, section.address, section.address + section.size, ); Ok(SectionAddress::new(section_index, self.address)) } } impl<'de> serde::Deserialize<'de> for SectionAddressRef { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de> { struct SectionAddressRefVisitor; impl<'de> serde::de::Visitor<'de> for SectionAddressRefVisitor { type Value = SectionAddressRef; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a section address reference") } fn visit_u64(self, v: u64) -> Result where E: serde::de::Error { Ok(SectionAddressRef::new(None, v.try_into().map_err(serde::de::Error::custom)?)) } fn visit_str(self, value: &str) -> Result where E: serde::de::Error { let mut parts = value.splitn(2, ':'); let section = parts.next().map(|s| s.to_string()); let address = parts.next().ok_or_else(|| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(value), &self) })?; let address = parse_u32(address).map_err(serde::de::Error::custom)?; Ok(SectionAddressRef::new(section, address)) } } deserializer.deserialize_any(SectionAddressRefVisitor) } } impl serde::Serialize for SectionAddressRef { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer { if let Some(section) = &self.section { serializer.serialize_str(&format!("{}:{:#X}", section, self.address)) } else { serializer.serialize_str(&format!("{:#X}", self.address)) } } }