use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjCopyFormat {
ELF,
MachO,
COFF,
Wasm,
Binary,
Unknown,
}
impl ObjCopyFormat {
pub fn from_magic(bytes: &[u8]) -> Self {
if bytes.len() < 4 {
return ObjCopyFormat::Unknown;
}
match &bytes[0..4] {
[0x7f, b'E', b'L', b'F'] => ObjCopyFormat::ELF,
[0xFE, 0xED, 0xFA, 0xCE]
| [0xCF, 0xFA, 0xED, 0xFE]
| [0xFE, 0xED, 0xFA, 0xCF]
| [0xCE, 0xFA, 0xED, 0xFE] => ObjCopyFormat::MachO,
[b'M', b'Z', ..] => ObjCopyFormat::COFF,
[0x00, b'a', b's', b'm'] => ObjCopyFormat::Wasm,
_ => ObjCopyFormat::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StripMode {
None,
StripAll, StripDebug, StripUnneeded, StripSections, DiscardAll, DiscardLocals, KeepFileSymbols, }
#[derive(Debug, Clone)]
pub struct SectionPattern {
pattern: String,
is_wildcard: bool,
}
impl SectionPattern {
pub fn new(pattern: &str) -> Self {
Self {
pattern: pattern.to_string(),
is_wildcard: pattern.contains('*'),
}
}
pub fn matches(&self, name: &str) -> bool {
if self.is_wildcard {
let re_pattern = self.pattern.replace("*", ".*");
name.contains(&re_pattern.replace(".*", "")) || name == self.pattern.trim_matches('*')
} else {
name == self.pattern
}
}
}
#[derive(Debug, Clone)]
pub struct ElfSection {
pub name: String,
pub sh_type: u32,
pub sh_flags: u64,
pub sh_addr: u64,
pub sh_offset: u64,
pub sh_size: u64,
pub sh_link: u32,
pub sh_info: u32,
pub sh_addralign: u64,
pub sh_entsize: u64,
pub data: Vec<u8>,
pub is_debug: bool,
}
impl ElfSection {
pub fn new(name: &str, sh_type: u32, flags: u64) -> Self {
Self {
name: name.to_string(),
sh_type,
sh_flags: flags,
sh_addr: 0,
sh_offset: 0,
sh_size: 0,
sh_link: 0,
sh_info: 0,
sh_addralign: 1,
sh_entsize: 0,
data: Vec::new(),
is_debug: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ElfSymbol {
pub name: String,
pub st_info: u8,
pub st_other: u8,
pub st_shndx: u16,
pub st_value: u64,
pub st_size: u64,
}
impl ElfSymbol {
pub fn is_debug(&self) -> bool {
self.st_shndx == 0xFFFF && self.st_info == 0
}
pub fn is_local(&self) -> bool {
(self.st_info >> 4) == 0
} pub fn is_global(&self) -> bool {
(self.st_info >> 4) == 1
} pub fn is_weak(&self) -> bool {
(self.st_info >> 4) == 2
} pub fn is_undefined(&self) -> bool {
self.st_shndx == 0
}
}
#[derive(Debug, Clone)]
pub struct ObjCopyObject {
pub format: ObjCopyFormat,
pub is_64bit: bool,
pub is_little_endian: bool,
pub sections: Vec<ElfSection>,
pub symbols: Vec<ElfSymbol>,
pub raw_header: Vec<u8>,
pub raw_data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct ObjCopyConfig {
pub input_file: String,
pub output_file: Option<String>,
pub output_format: Option<ObjCopyFormat>,
pub strip_mode: StripMode,
pub remove_sections: Vec<SectionPattern>,
pub keep_sections: Vec<SectionPattern>,
pub add_section: Vec<(String, String)>, pub rename_section: Vec<(String, String)>, pub set_section_flags: Vec<(String, String)>, pub add_symbol: Vec<(String, u64, String)>, pub remove_symbols: Vec<String>,
pub keep_symbols: Vec<String>,
pub add_gnu_debuglink: Option<String>,
pub only_keep_debug: bool,
pub extract_dwo: bool,
pub binary_output: bool,
pub verbose: bool,
}
impl Default for ObjCopyConfig {
fn default() -> Self {
Self {
input_file: String::new(),
output_file: None,
output_format: None,
strip_mode: StripMode::None,
remove_sections: Vec::new(),
keep_sections: Vec::new(),
add_section: Vec::new(),
rename_section: Vec::new(),
set_section_flags: Vec::new(),
add_symbol: Vec::new(),
remove_symbols: Vec::new(),
keep_symbols: Vec::new(),
add_gnu_debuglink: None,
only_keep_debug: false,
extract_dwo: false,
binary_output: false,
verbose: false,
}
}
}
pub struct ObjCopy {
config: ObjCopyConfig,
}
impl ObjCopy {
pub fn new(config: ObjCopyConfig) -> Self {
Self { config }
}
pub fn execute(&self) -> Result<(), String> {
let data = fs::read(&self.config.input_file)
.map_err(|e| format!("cannot read '{}': {}", self.config.input_file, e))?;
let format = ObjCopyFormat::from_magic(&data);
let mut obj = match format {
ObjCopyFormat::ELF => self.parse_elf(&data)?,
_ => return Err(format!("unsupported format: {:?}", format)),
};
self.remove_sections(&mut obj);
self.rename_sections(&mut obj);
let stripped_count = self.strip_symbols(&mut obj);
self.add_sections_from_files(&mut obj)?;
for (name, val, sec) in &self.config.add_symbol {
obj.symbols.push(ElfSymbol {
name: name.clone(),
st_info: 0x12,
st_other: 0,
st_shndx: 1,
st_value: *val,
st_size: 0,
});
}
let output = self.rebuild_elf(&obj);
let output_path = self
.config
.output_file
.as_deref()
.unwrap_or(&self.config.input_file);
fs::write(output_path, &output)
.map_err(|e| format!("cannot write '{}': {}", output_path, e))?;
if self.config.verbose {
eprintln!(
"objcopy: {} -> {} ({} symbols stripped)",
self.config.input_file, output_path, stripped_count
);
}
Ok(())
}
fn parse_elf(&self, data: &[u8]) -> Result<ObjCopyObject, String> {
if data.len() < 64 {
return Err("ELF too small".into());
}
let is_64bit = data[4] == 2;
let is_le = data[5] == 1;
Ok(ObjCopyObject {
format: ObjCopyFormat::ELF,
is_64bit,
is_little_endian: is_le,
sections: Vec::new(),
symbols: Vec::new(),
raw_header: data[..64].to_vec(),
raw_data: data.to_vec(),
})
}
fn remove_sections(&self, obj: &mut ObjCopyObject) {
if self.config.remove_sections.is_empty() {
return;
}
obj.sections.retain(|sec| {
!self
.config
.remove_sections
.iter()
.any(|pat| pat.matches(&sec.name))
});
}
fn rename_sections(&self, obj: &mut ObjCopyObject) {
for (old, new) in &self.config.rename_section {
for sec in &mut obj.sections {
if sec.name == *old {
sec.name = new.clone();
}
}
}
}
fn strip_symbols(&self, obj: &mut ObjCopyObject) -> usize {
let before = obj.symbols.len();
match self.config.strip_mode {
StripMode::None => return 0,
StripMode::StripDebug => {
obj.symbols.retain(|s| !s.is_debug());
}
StripMode::StripAll => {
obj.symbols.clear();
}
StripMode::StripUnneeded => {
obj.symbols
.retain(|s| s.is_global() || s.is_weak() || s.is_undefined());
}
StripMode::DiscardAll => {
obj.symbols.retain(|s| !s.is_local());
}
StripMode::DiscardLocals => {
obj.symbols.retain(|s| {
if s.is_local() && s.name.starts_with(".L") {
false
} else {
true
}
});
}
StripMode::KeepFileSymbols => {
obj.symbols
.retain(|s| !s.is_debug() && s.st_shndx != 0xFFF1);
}
StripMode::StripSections => {
obj.sections.clear();
}
}
before - obj.symbols.len()
}
fn add_sections_from_files(&self, obj: &mut ObjCopyObject) -> Result<(), String> {
for (sec_name, file_path) in &self.config.add_section {
let data = fs::read(file_path)
.map_err(|e| format!("cannot read section data '{}': {}", file_path, e))?;
let mut sec = ElfSection::new(sec_name, 1, 0); sec.data = data;
sec.sh_size = sec.data.len() as u64;
obj.sections.push(sec);
}
Ok(())
}
fn rebuild_elf(&self, obj: &ObjCopyObject) -> Vec<u8> {
let mut output = Vec::new();
output.extend_from_slice(&obj.raw_header);
if self.config.strip_mode == StripMode::StripSections {
let e_shoff = if obj.is_64bit { 40 } else { 32 };
for i in e_shoff..e_shoff + 8 {
if i < output.len() {
output[i] = 0;
}
}
let e_shnum = if obj.is_64bit { 60 } else { 48 };
for i in e_shnum..e_shnum + 2 {
if i < output.len() {
output[i] = 0;
}
}
}
if self.config.binary_output {
let mut binary = Vec::new();
for sec in &obj.sections {
if sec.sh_flags & 0x2 != 0 {
binary.extend_from_slice(&sec.data);
}
}
return binary;
}
output
}
}
pub struct LlvmStrip;
impl LlvmStrip {
pub fn strip_all(input: &str) -> Result<(), String> {
let config = ObjCopyConfig {
input_file: input.to_string(),
strip_mode: StripMode::StripAll,
..Default::default()
};
ObjCopy::new(config).execute()
}
pub fn strip_debug(input: &str) -> Result<(), String> {
let config = ObjCopyConfig {
input_file: input.to_string(),
strip_mode: StripMode::StripDebug,
..Default::default()
};
ObjCopy::new(config).execute()
}
pub fn strip_unneeded(input: &str) -> Result<(), String> {
let config = ObjCopyConfig {
input_file: input.to_string(),
strip_mode: StripMode::StripUnneeded,
..Default::default()
};
ObjCopy::new(config).execute()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_detection_elf() {
let data = vec![0x7f, b'E', b'L', b'F', 2, 1, 1, 0];
assert_eq!(ObjCopyFormat::from_magic(&data), ObjCopyFormat::ELF);
}
#[test]
fn test_section_pattern_exact() {
let pat = SectionPattern::new(".debug_info");
assert!(pat.matches(".debug_info"));
assert!(!pat.matches(".debug_line"));
}
#[test]
fn test_section_pattern_wildcard() {
let pat = SectionPattern::new(".debug_*");
assert!(pat.matches(".debug_info"));
assert!(pat.matches(".debug_line"));
assert!(!pat.matches(".text"));
}
#[test]
fn test_strip_debug_symbols() {
let mut obj = ObjCopyObject {
format: ObjCopyFormat::ELF,
is_64bit: true,
is_little_endian: true,
sections: Vec::new(),
symbols: vec![
ElfSymbol {
name: "main".into(),
st_info: 0x12,
st_other: 0,
st_shndx: 1,
st_value: 0x1000,
st_size: 64,
},
ElfSymbol {
name: "debug_info".into(),
st_info: 0,
st_other: 0,
st_shndx: 0xFFFF,
st_value: 0,
st_size: 0,
},
],
raw_header: vec![0u8; 64],
raw_data: Vec::new(),
};
let config = ObjCopyConfig {
strip_mode: StripMode::StripDebug,
..Default::default()
};
let oc = ObjCopy::new(config);
let count = oc.strip_symbols(&mut obj);
assert_eq!(count, 1);
assert_eq!(obj.symbols.len(), 1);
assert_eq!(obj.symbols[0].name, "main");
}
#[test]
fn test_strip_all() {
let mut obj = ObjCopyObject {
format: ObjCopyFormat::ELF,
is_64bit: true,
is_little_endian: true,
sections: Vec::new(),
symbols: vec![
ElfSymbol {
name: "main".into(),
st_info: 0x12,
st_other: 0,
st_shndx: 1,
st_value: 0x1000,
st_size: 64,
},
ElfSymbol {
name: "helper".into(),
st_info: 0x12,
st_other: 0,
st_shndx: 1,
st_value: 0x1100,
st_size: 32,
},
],
raw_header: vec![0u8; 64],
raw_data: Vec::new(),
};
let config = ObjCopyConfig {
strip_mode: StripMode::StripAll,
..Default::default()
};
let oc = ObjCopy::new(config);
oc.strip_symbols(&mut obj);
assert!(obj.symbols.is_empty());
}
#[test]
fn test_strip_unneeded() {
let mut obj = ObjCopyObject {
format: ObjCopyFormat::ELF,
is_64bit: true,
is_little_endian: true,
sections: Vec::new(),
symbols: vec![
ElfSymbol {
name: "global_fn".into(),
st_info: 0x12,
st_other: 0,
st_shndx: 1,
st_value: 0x1000,
st_size: 64,
},
ElfSymbol {
name: "ext_ref".into(),
st_info: 0x10,
st_other: 0,
st_shndx: 0,
st_value: 0,
st_size: 0,
},
ElfSymbol {
name: "local_tmp".into(),
st_info: 0x02,
st_other: 0,
st_shndx: 1,
st_value: 0x1200,
st_size: 16,
},
],
raw_header: vec![0u8; 64],
raw_data: Vec::new(),
};
let config = ObjCopyConfig {
strip_mode: StripMode::StripUnneeded,
..Default::default()
};
let oc = ObjCopy::new(config);
oc.strip_symbols(&mut obj);
assert_eq!(obj.symbols.len(), 2); }
#[test]
fn test_binary_output() {
let config = ObjCopyConfig {
binary_output: true,
..Default::default()
};
let oc = ObjCopy::new(config);
let mut obj = ObjCopyObject {
format: ObjCopyFormat::ELF,
is_64bit: true,
is_little_endian: true,
sections: vec![
ElfSection {
name: ".text".into(),
sh_type: 1,
sh_flags: 0x6,
sh_addr: 0x1000,
sh_offset: 0,
sh_size: 4,
sh_link: 0,
sh_info: 0,
sh_addralign: 16,
sh_entsize: 0,
data: vec![0x90, 0x90, 0x90, 0xC3],
is_debug: false,
},
ElfSection {
name: ".debug_info".into(),
sh_type: 1,
sh_flags: 0,
sh_addr: 0,
sh_offset: 0,
sh_size: 10,
sh_link: 0,
sh_info: 0,
sh_addralign: 1,
sh_entsize: 0,
data: vec![0u8; 10],
is_debug: true,
},
],
symbols: Vec::new(),
raw_header: vec![0u8; 64],
raw_data: Vec::new(),
};
let output = oc.rebuild_elf(&obj);
assert_eq!(output, vec![0x90, 0x90, 0x90, 0xC3]);
}
#[test]
fn test_rename_section() {
let config = ObjCopyConfig {
rename_section: vec![(".text".into(), ".code".into())],
..Default::default()
};
let oc = ObjCopy::new(config);
let mut obj = ObjCopyObject {
format: ObjCopyFormat::ELF,
is_64bit: true,
is_little_endian: true,
sections: vec![ElfSection::new(".text", 1, 6)],
symbols: Vec::new(),
raw_header: vec![0u8; 64],
raw_data: Vec::new(),
};
oc.rename_sections(&mut obj);
assert_eq!(obj.sections[0].name, ".code");
}
}