1use std::ffi::{CStr, OsString};
16use std::path::Path;
17
18use anyhow::{Context, Result, bail};
19use goblin::container;
20use goblin::mach::fat;
21use goblin::mach::header::{Header, SIZEOF_HEADER_32, SIZEOF_HEADER_64};
22use goblin::mach::load_command::{
23 CommandVariant, DylibCommand, LC_RPATH, LoadCommand, RpathCommand, SIZEOF_RPATH_COMMAND,
24};
25use goblin::mach::{MachO, MultiArch, parse_magic_and_ctx, peek};
26use scroll::Pwrite;
27
28#[derive(Debug, Default)]
30struct Args {
31 id: Option<String>,
32 changes: Vec<(String, String)>,
33 rpaths: Vec<(String, String)>,
34 add_rpaths: Vec<String>,
35 delete_rpaths: Vec<String>,
36 input: Option<String>,
37}
38
39fn parse_args(args: &[String]) -> Result<Args> {
40 let mut parsed = Args::default();
41 let mut i = 0;
42 while i < args.len() {
43 match args[i].as_str() {
44 "-id" => {
45 if i + 1 >= args.len() {
46 bail!("-id requires an argument");
47 }
48 parsed.id = Some(args[i + 1].clone());
49 i += 2;
50 }
51 "-change" => {
52 if i + 2 >= args.len() {
53 bail!("-change requires two arguments");
54 }
55 parsed
56 .changes
57 .push((args[i + 1].clone(), args[i + 2].clone()));
58 i += 3;
59 }
60 "-rpath" => {
61 if i + 2 >= args.len() {
62 bail!("-rpath requires two arguments");
63 }
64 parsed
65 .rpaths
66 .push((args[i + 1].clone(), args[i + 2].clone()));
67 i += 3;
68 }
69 "-add_rpath" => {
70 if i + 1 >= args.len() {
71 bail!("-add_rpath requires an argument");
72 }
73 parsed.add_rpaths.push(args[i + 1].clone());
74 i += 2;
75 }
76 "-delete_rpath" => {
77 if i + 1 >= args.len() {
78 bail!("-delete_rpath requires an argument");
79 }
80 parsed.delete_rpaths.push(args[i + 1].clone());
81 i += 2;
82 }
83 arg if arg.starts_with('-') => {
84 bail!("unknown option: {arg}");
85 }
86 _ => {
87 if parsed.input.is_some() {
88 bail!("multiple input files not supported");
89 }
90 parsed.input = Some(args[i].clone());
91 i += 1;
92 }
93 }
94 }
95 if parsed.input.is_none() {
96 bail!("no input file specified");
97 }
98 Ok(parsed)
99}
100
101fn header_size(ctx: container::Ctx) -> usize {
104 if ctx.container.is_big() {
105 SIZEOF_HEADER_64
106 } else {
107 SIZEOF_HEADER_32
108 }
109}
110
111fn align_to_ctx(size: usize, ctx: container::Ctx) -> usize {
113 if ctx.container.is_big() {
114 size.next_multiple_of(8)
115 } else {
116 size.next_multiple_of(4)
117 }
118}
119
120fn remove_load_command(
124 buffer: &mut Vec<u8>,
125 header: &mut Header,
126 ctx: container::Ctx,
127 cmd_offset: usize,
128 cmdsize: usize,
129) -> Result<()> {
130 buffer.drain(cmd_offset..cmd_offset + cmdsize);
131
132 header.ncmds -= 1;
133 header.sizeofcmds -= cmdsize as u32;
134
135 let padding_offset = header_size(ctx) + header.sizeofcmds as usize;
137 let zeroes = vec![0u8; cmdsize];
138 let tail = buffer.split_off(padding_offset);
139 buffer.extend(&zeroes);
140 buffer.extend(tail);
141
142 buffer.pwrite_with(*header, 0, ctx)?;
143 Ok(())
144}
145
146fn insert_load_command(
148 buffer: &mut Vec<u8>,
149 header: &mut Header,
150 ctx: container::Ctx,
151 offset: usize,
152 cmd_data: &[u8],
153) -> Result<()> {
154 let new_cmd_size = cmd_data.len() as u32;
155
156 header.ncmds += 1;
157 header.sizeofcmds += new_cmd_size;
158
159 let tail = buffer.split_off(offset);
161 buffer.extend_from_slice(cmd_data);
162 buffer.extend(tail);
163
164 let drain_start = header_size(ctx) + header.sizeofcmds as usize;
166 let drain_end = drain_start + new_cmd_size as usize;
167 if drain_end <= buffer.len() {
168 buffer.drain(drain_start..drain_end);
169 }
170
171 buffer.pwrite_with(*header, 0, ctx)?;
172 Ok(())
173}
174
175fn build_rpath_command(path: &str, ctx: container::Ctx) -> Result<(RpathCommand, Vec<u8>)> {
177 let c_str = format!("{path}\0");
178 let c_str = CStr::from_bytes_with_nul(c_str.as_bytes())?;
179 let str_size = (c_str.count_bytes() + 1).next_multiple_of(4);
180 let cmdsize = align_to_ctx(SIZEOF_RPATH_COMMAND + str_size, ctx);
181
182 let rpath_cmd = RpathCommand {
183 cmd: LC_RPATH,
184 cmdsize: cmdsize as u32,
185 path: SIZEOF_RPATH_COMMAND as u32,
186 };
187
188 let mut buf = vec![0u8; cmdsize];
189 buf.pwrite(rpath_cmd, 0)?;
190 buf.pwrite(c_str, SIZEOF_RPATH_COMMAND)?;
191 Ok((rpath_cmd, buf))
192}
193
194fn build_dylib_command(
196 name: &str,
197 old_cmd: &DylibCommand,
198 ctx: container::Ctx,
199) -> Result<(DylibCommand, Vec<u8>)> {
200 let c_str = format!("{name}\0");
201 let c_str = CStr::from_bytes_with_nul(c_str.as_bytes())?;
202 let str_size = (c_str.count_bytes() + 1).next_multiple_of(4);
203 let dylib_header_size: usize = 24;
205 let cmdsize = align_to_ctx(dylib_header_size + str_size, ctx);
206
207 let new_cmd = DylibCommand {
208 cmd: old_cmd.cmd,
209 cmdsize: cmdsize as u32,
210 dylib: goblin::mach::load_command::Dylib {
211 name: dylib_header_size as u32,
212 timestamp: old_cmd.dylib.timestamp,
213 current_version: old_cmd.dylib.current_version,
214 compatibility_version: old_cmd.dylib.compatibility_version,
215 },
216 };
217
218 let mut buf = vec![0u8; cmdsize];
219 buf.pwrite(new_cmd, 0)?;
220 buf.pwrite(c_str, dylib_header_size)?;
221 Ok((new_cmd, buf))
222}
223
224fn read_dylib_name<'a>(data: &'a [u8], lc: &LoadCommand, dylib_cmd: &DylibCommand) -> &'a str {
228 let name_offset = lc.offset + dylib_cmd.dylib.name as usize;
229 let cmd_end = lc.offset + dylib_cmd.cmdsize as usize;
230 let name_end = data[name_offset..cmd_end]
231 .iter()
232 .position(|&b| b == 0)
233 .map(|p| name_offset + p)
234 .unwrap_or(cmd_end);
235 std::str::from_utf8(&data[name_offset..name_end]).unwrap_or("")
236}
237
238fn read_rpath_path<'a>(data: &'a [u8], lc: &LoadCommand, rpath_cmd: &RpathCommand) -> &'a str {
240 let path_offset = lc.offset + rpath_cmd.path as usize;
241 let cmd_end = lc.offset + rpath_cmd.cmdsize as usize;
242 let path_end = data[path_offset..cmd_end]
243 .iter()
244 .position(|&b| b == 0)
245 .map(|p| path_offset + p)
246 .unwrap_or(cmd_end);
247 std::str::from_utf8(&data[path_offset..path_end]).unwrap_or("")
248}
249
250fn process_single_macho(data: &mut Vec<u8>, args: &Args) -> Result<()> {
254 let macho = MachO::parse(data, 0).context("failed to parse Mach-O")?;
255 let (_, maybe_ctx) = parse_magic_and_ctx(data, 0)?;
256 let ctx = maybe_ctx.context("could not determine endianness")?;
257 let mut header = macho.header;
258
259 if let Some(ref new_id) = args.id {
261 let mut found = false;
262 for lc in &macho.load_commands {
263 if let CommandVariant::IdDylib(ref dylib_cmd) = lc.command {
264 let cmdsize = lc.command.cmdsize();
265 let (_, new_cmd_buf) = build_dylib_command(new_id, dylib_cmd, ctx)?;
266 remove_load_command(data, &mut header, ctx, lc.offset, cmdsize)?;
267 insert_load_command(data, &mut header, ctx, lc.offset, &new_cmd_buf)?;
268 found = true;
269 break;
270 }
271 }
272 if !found {
273 bail!("no LC_ID_DYLIB found in binary");
274 }
275 }
276
277 for (old_name, new_name) in &args.changes {
282 let macho = MachO::parse(data, 0).context("failed to re-parse Mach-O")?;
283 let (_, maybe_ctx) = parse_magic_and_ctx(data, 0)?;
284 let ctx = maybe_ctx.context("could not determine endianness")?;
285 let mut header = macho.header;
286
287 let mut found = false;
288 for lc in &macho.load_commands {
289 let dylib_cmd = match &lc.command {
290 CommandVariant::LoadDylib(cmd)
291 | CommandVariant::LoadWeakDylib(cmd)
292 | CommandVariant::ReexportDylib(cmd)
293 | CommandVariant::LazyLoadDylib(cmd)
294 | CommandVariant::LoadUpwardDylib(cmd) => cmd,
295 _ => continue,
296 };
297 let name = read_dylib_name(data, lc, dylib_cmd);
298 if name == old_name.as_str() {
299 let cmdsize = lc.command.cmdsize();
300 let (_, new_cmd_buf) = build_dylib_command(new_name, dylib_cmd, ctx)?;
301 remove_load_command(data, &mut header, ctx, lc.offset, cmdsize)?;
302 insert_load_command(data, &mut header, ctx, lc.offset, &new_cmd_buf)?;
303 found = true;
304 break;
305 }
306 }
307 if !found {
308 bail!("no LC_LOAD_DYLIB with name '{old_name}' found");
309 }
310 }
311
312 for (old_rpath, new_rpath) in &args.rpaths {
314 let macho = MachO::parse(data, 0).context("failed to re-parse Mach-O")?;
315 let (_, maybe_ctx) = parse_magic_and_ctx(data, 0)?;
316 let ctx = maybe_ctx.context("could not determine endianness")?;
317 let mut header = macho.header;
318
319 let mut found = false;
320 for lc in &macho.load_commands {
321 if let CommandVariant::Rpath(ref rpath_cmd) = lc.command {
322 let path = read_rpath_path(data, lc, rpath_cmd);
323 if path == old_rpath.as_str() {
324 let cmdsize = lc.command.cmdsize();
325 let (_, new_cmd_buf) = build_rpath_command(new_rpath, ctx)?;
326 remove_load_command(data, &mut header, ctx, lc.offset, cmdsize)?;
327 insert_load_command(data, &mut header, ctx, lc.offset, &new_cmd_buf)?;
328 found = true;
329 break;
330 }
331 }
332 }
333 if !found {
334 bail!("no LC_RPATH with path '{old_rpath}' found");
335 }
336 }
337
338 for del_rpath in &args.delete_rpaths {
340 let macho = MachO::parse(data, 0).context("failed to re-parse Mach-O")?;
341 let (_, maybe_ctx) = parse_magic_and_ctx(data, 0)?;
342 let ctx = maybe_ctx.context("could not determine endianness")?;
343 let mut header = macho.header;
344
345 let mut found = false;
346 for lc in &macho.load_commands {
347 if let CommandVariant::Rpath(ref rpath_cmd) = lc.command {
348 let path = read_rpath_path(data, lc, rpath_cmd);
349 if path == del_rpath.as_str() {
350 let cmdsize = lc.command.cmdsize();
351 remove_load_command(data, &mut header, ctx, lc.offset, cmdsize)?;
352 found = true;
353 break;
354 }
355 }
356 }
357 if !found {
358 bail!("no LC_RPATH with path '{del_rpath}' found");
359 }
360 }
361
362 for new_rpath in &args.add_rpaths {
364 let macho = MachO::parse(data, 0).context("failed to re-parse Mach-O")?;
365 let (_, maybe_ctx) = parse_magic_and_ctx(data, 0)?;
366 let ctx = maybe_ctx.context("could not determine endianness")?;
367 let mut header = macho.header;
368
369 let insert_offset = header_size(ctx) + header.sizeofcmds as usize;
370 let (_, new_cmd_buf) = build_rpath_command(new_rpath, ctx)?;
371 insert_load_command(data, &mut header, ctx, insert_offset, &new_cmd_buf)?;
372 }
373
374 Ok(())
375}
376
377fn process_file(path: &Path, args: &Args) -> Result<()> {
380 let mut data =
381 fs_err::read(path).with_context(|| format!("failed to read '{}'", path.display()))?;
382
383 let magic = peek(&data, 0)?;
384
385 match magic {
386 fat::FAT_MAGIC => {
387 let multi = MultiArch::new(&data)?;
388 let arches: Vec<_> = multi.iter_arches().collect::<std::result::Result<_, _>>()?;
389
390 for arch in arches.iter().rev() {
393 let offset = arch.offset as usize;
394 let size = arch.size as usize;
395 let mut slice = data[offset..offset + size].to_vec();
396 process_single_macho(&mut slice, args)?;
397 data.splice(offset..offset + size, slice);
398 }
399 }
400 _ => {
401 process_single_macho(&mut data, args)?;
403 }
404 }
405
406 fs_err::write(path, &data).with_context(|| format!("failed to write '{}'", path.display()))?;
407
408 Ok(())
409}
410
411pub fn execute(args: impl IntoIterator<Item = impl Into<OsString>>) -> Result<()> {
413 let args: Vec<String> = args
414 .into_iter()
415 .map(|a| a.into().to_string_lossy().into_owned())
416 .collect();
417 let parsed = parse_args(&args)?;
418 let input = parsed.input.as_ref().unwrap();
419 process_file(Path::new(input), &parsed)
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use std::path::PathBuf;
426
427 fn fixtures_dir() -> PathBuf {
428 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
429 }
430
431 fn copy_fixture(name: &str) -> tempfile::NamedTempFile {
433 let src = fixtures_dir().join(name);
434 let mut tmp = tempfile::NamedTempFile::new().unwrap();
435 std::io::copy(&mut fs_err::File::open(src).unwrap(), &mut tmp).unwrap();
436 tmp
437 }
438
439 fn read_id(data: &[u8]) -> Option<String> {
441 let macho = MachO::parse(data, 0).unwrap();
442 macho.name.map(|s| s.to_string())
443 }
444
445 fn read_rpaths(data: &[u8]) -> Vec<String> {
447 let macho = MachO::parse(data, 0).unwrap();
448 macho.rpaths.iter().map(|s| s.to_string()).collect()
449 }
450
451 fn fat_slices(data: &[u8]) -> Vec<(usize, usize)> {
453 let multi = MultiArch::new(data).unwrap();
454 multi
455 .iter_arches()
456 .map(|a| {
457 let a = a.unwrap();
458 (a.offset as usize, a.size as usize)
459 })
460 .collect()
461 }
462
463 #[test]
466 fn test_change_id_aarch64() {
467 let tmp = copy_fixture("test_aarch64.dylib");
468 execute(["-id", "/new/lib/test.dylib", tmp.path().to_str().unwrap()]).unwrap();
469
470 let data = fs_err::read(tmp.path()).unwrap();
471 assert_eq!(read_id(&data).as_deref(), Some("/new/lib/test.dylib"));
472 }
473
474 #[test]
475 fn test_change_rpath_aarch64() {
476 let tmp = copy_fixture("test_aarch64.dylib");
477 execute([
478 "-rpath",
479 "/old/rpath",
480 "/new/rpath",
481 tmp.path().to_str().unwrap(),
482 ])
483 .unwrap();
484
485 let data = fs_err::read(tmp.path()).unwrap();
486 let rpaths = read_rpaths(&data);
487 assert_eq!(rpaths, vec!["/new/rpath"]);
488 }
489
490 #[test]
491 fn test_delete_rpath_aarch64() {
492 let tmp = copy_fixture("test_aarch64.dylib");
493 execute(["-delete_rpath", "/old/rpath", tmp.path().to_str().unwrap()]).unwrap();
494
495 let data = fs_err::read(tmp.path()).unwrap();
496 let rpaths = read_rpaths(&data);
497 assert!(rpaths.is_empty());
498 }
499
500 #[test]
501 fn test_add_rpath_aarch64() {
502 let tmp = copy_fixture("test_aarch64.dylib");
503 execute(["-add_rpath", "/added/rpath", tmp.path().to_str().unwrap()]).unwrap();
504
505 let data = fs_err::read(tmp.path()).unwrap();
506 let rpaths = read_rpaths(&data);
507 assert!(rpaths.contains(&"/old/rpath".to_string()));
508 assert!(rpaths.contains(&"/added/rpath".to_string()));
509 }
510
511 #[test]
514 fn test_change_id_x86_64() {
515 let tmp = copy_fixture("test_x86_64.dylib");
516 execute(["-id", "/new/lib/test.dylib", tmp.path().to_str().unwrap()]).unwrap();
517
518 let data = fs_err::read(tmp.path()).unwrap();
519 assert_eq!(read_id(&data).as_deref(), Some("/new/lib/test.dylib"));
520 }
521
522 #[test]
523 fn test_change_rpath_x86_64() {
524 let tmp = copy_fixture("test_x86_64.dylib");
525 execute([
526 "-rpath",
527 "/old/rpath",
528 "/new/rpath",
529 tmp.path().to_str().unwrap(),
530 ])
531 .unwrap();
532
533 let data = fs_err::read(tmp.path()).unwrap();
534 let rpaths = read_rpaths(&data);
535 assert_eq!(rpaths, vec!["/new/rpath"]);
536 }
537
538 #[test]
541 fn test_change_id_universal2() {
542 let tmp = copy_fixture("test_universal2.dylib");
543 execute(["-id", "/new/lib/test.dylib", tmp.path().to_str().unwrap()]).unwrap();
544
545 let data = fs_err::read(tmp.path()).unwrap();
546 for (offset, size) in fat_slices(&data) {
547 let slice = &data[offset..offset + size];
548 assert_eq!(read_id(slice).as_deref(), Some("/new/lib/test.dylib"));
549 }
550 }
551
552 #[test]
553 fn test_change_rpath_universal2() {
554 let tmp = copy_fixture("test_universal2.dylib");
555 execute([
556 "-rpath",
557 "/old/rpath",
558 "/new/rpath",
559 tmp.path().to_str().unwrap(),
560 ])
561 .unwrap();
562
563 let data = fs_err::read(tmp.path()).unwrap();
564 for (offset, size) in fat_slices(&data) {
565 let slice = &data[offset..offset + size];
566 assert_eq!(read_rpaths(slice), vec!["/new/rpath"]);
567 }
568 }
569
570 #[test]
571 fn test_delete_rpath_universal2() {
572 let tmp = copy_fixture("test_universal2.dylib");
573 execute(["-delete_rpath", "/old/rpath", tmp.path().to_str().unwrap()]).unwrap();
574
575 let data = fs_err::read(tmp.path()).unwrap();
576 for (offset, size) in fat_slices(&data) {
577 let slice = &data[offset..offset + size];
578 assert!(read_rpaths(slice).is_empty());
579 }
580 }
581
582 #[test]
583 fn test_add_rpath_universal2() {
584 let tmp = copy_fixture("test_universal2.dylib");
585 execute(["-add_rpath", "/added/rpath", tmp.path().to_str().unwrap()]).unwrap();
586
587 let data = fs_err::read(tmp.path()).unwrap();
588 for (offset, size) in fat_slices(&data) {
589 let slice = &data[offset..offset + size];
590 let rpaths = read_rpaths(slice);
591 assert!(rpaths.contains(&"/old/rpath".to_string()));
592 assert!(rpaths.contains(&"/added/rpath".to_string()));
593 }
594 }
595
596 #[test]
599 fn test_multiple_operations_aarch64() {
600 let tmp = copy_fixture("test_aarch64.dylib");
601 execute(["-id", "/new/id.dylib", tmp.path().to_str().unwrap()]).unwrap();
603 execute([
604 "-rpath",
605 "/old/rpath",
606 "/replaced/rpath",
607 tmp.path().to_str().unwrap(),
608 ])
609 .unwrap();
610 execute(["-add_rpath", "/extra/rpath", tmp.path().to_str().unwrap()]).unwrap();
611
612 let data = fs_err::read(tmp.path()).unwrap();
613 assert_eq!(read_id(&data).as_deref(), Some("/new/id.dylib"));
614 let rpaths = read_rpaths(&data);
615 assert!(rpaths.contains(&"/replaced/rpath".to_string()));
616 assert!(rpaths.contains(&"/extra/rpath".to_string()));
617 assert!(!rpaths.contains(&"/old/rpath".to_string()));
618 }
619
620 #[test]
623 fn test_delete_nonexistent_rpath_fails() {
624 let tmp = copy_fixture("test_aarch64.dylib");
625 let result = execute([
626 "-delete_rpath",
627 "/nonexistent",
628 tmp.path().to_str().unwrap(),
629 ]);
630 assert!(result.is_err());
631 }
632
633 #[test]
634 fn test_change_nonexistent_rpath_fails() {
635 let tmp = copy_fixture("test_aarch64.dylib");
636 let result = execute([
637 "-rpath",
638 "/nonexistent",
639 "/new",
640 tmp.path().to_str().unwrap(),
641 ]);
642 assert!(result.is_err());
643 }
644}