diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..c095fd5 --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,224 @@ +#![allow(dead_code)] + +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use ar_archive_writer::{ArchiveKind, NewArchiveMember}; +use object::write::{self, Object}; +use object::{Architecture, BinaryFormat, Endianness, SymbolFlags, SymbolKind, SymbolScope}; +use pretty_assertions::assert_eq; + +/// Creates the temporary directory for a test. +pub fn create_tmp_dir(test_name: &str) -> PathBuf { + let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(test_name); + match fs::remove_dir_all(&tmpdir) { + Ok(_) => {} + Err(err) => { + if err.kind() != std::io::ErrorKind::NotFound { + panic!("Failed to delete directory: {:?}", tmpdir); + } + } + } + fs::create_dir_all(&tmpdir).unwrap(); + tmpdir +} + +fn run_llvm_ar(object_paths: &[PathBuf], archive_path: &Path, archive_kind: ArchiveKind) { + let ar_path = cargo_binutils::Tool::Ar.path().unwrap(); + + let format_arg = match archive_kind { + ArchiveKind::Gnu => "gnu", + ArchiveKind::Darwin => "darwin", + ArchiveKind::AixBig => "bigarchive", + _ => panic!("unsupported archive kind: {:?}", archive_kind), + }; + + let output = Command::new(ar_path) + .arg(format!("--format={}", format_arg)) + .arg("rcs") + .arg(archive_path) + .args(object_paths) + .output() + .unwrap(); + assert_eq!( + String::from_utf8_lossy(&output.stderr), + "", + "llvm-ar failed. archive: {archive_path:?}" + ); +} + +/// Creates an archive with the given objects using `llvm-ar`. +/// The generated archive is written to disk as `output_llvm_ar.a`. +pub fn create_archive_with_llvm_ar<'a>( + tmpdir: &Path, + archive_kind: ArchiveKind, + input_objects: impl IntoIterator, +) -> Vec { + let archive_file_path = tmpdir.join("output_llvm_ar.a"); + + let input_file_paths = input_objects + .into_iter() + .map(|(name, bytes)| { + let input_file_path = tmpdir.join(name); + if name.contains('/') { + fs::create_dir_all(input_file_path.parent().unwrap()).unwrap(); + } + fs::write(&input_file_path, bytes).unwrap(); + input_file_path + }) + .collect::>(); + + run_llvm_ar(&input_file_paths, &archive_file_path, archive_kind); + fs::read(archive_file_path).unwrap() +} + +/// Creates an archive with the given objects using `ar_archive_writer`. +/// The generated archive is written to disk as `output_ar_archive_writer.a`. +pub fn create_archive_with_ar_archive_writer<'a>( + tmpdir: &Path, + archive_kind: ArchiveKind, + input_objects: impl IntoIterator, +) -> Vec { + let members = input_objects + .into_iter() + .map(|(name, bytes)| NewArchiveMember { + buf: Box::new(bytes) as Box>, + get_symbols: ar_archive_writer::get_native_object_symbols, + member_name: name + .rsplit_once('/') + .map_or(name, |(_, filename)| filename) + .to_string(), + mtime: 0, + uid: 0, + gid: 0, + perms: 0o644, + }) + .collect::>(); + let mut output_bytes = Cursor::new(Vec::new()); + ar_archive_writer::write_archive_to_stream( + &mut output_bytes, + &members, + true, + archive_kind, + true, + false, + ) + .unwrap(); + + let output_archive_bytes = output_bytes.into_inner(); + let ar_archive_writer_file_path = tmpdir.join("output_ar_archive_writer.a"); + fs::write(&ar_archive_writer_file_path, &output_archive_bytes).unwrap(); + output_archive_bytes +} + +/// Helper for comparing archives generated by `llvm-ar` and `ar_archive_writer` +/// across a variety of archive kinds and their relevant object formats. +pub fn generate_archive_and_compare(test_name: &str, generate_objects: F) +where + F: Fn(Architecture, Endianness, BinaryFormat) -> Vec<(&'static str, Vec)>, +{ + for (architecture, endianness, binary_format, archive_kind) in [ + // Elf + GNU + ( + Architecture::X86_64, + Endianness::Little, + BinaryFormat::Elf, + ArchiveKind::Gnu, + ), + ( + Architecture::I386, + Endianness::Little, + BinaryFormat::Elf, + ArchiveKind::Gnu, + ), + ( + Architecture::Aarch64, + Endianness::Little, + BinaryFormat::Elf, + ArchiveKind::Gnu, + ), + // AIX Big + ( + Architecture::PowerPc64, + Endianness::Big, + BinaryFormat::Elf, + ArchiveKind::AixBig, + ), + // PE + GNU + ( + Architecture::X86_64, + Endianness::Little, + BinaryFormat::Coff, + ArchiveKind::Gnu, + ), + ( + Architecture::I386, + Endianness::Little, + BinaryFormat::Coff, + ArchiveKind::Gnu, + ), + // MachO + ( + Architecture::X86_64, + Endianness::Little, + BinaryFormat::MachO, + ArchiveKind::Darwin, + ), + ( + Architecture::Aarch64, + Endianness::Little, + BinaryFormat::MachO, + ArchiveKind::Darwin, + ), + ] { + let tmpdir = create_tmp_dir(test_name); + let input_objects = generate_objects(architecture, endianness, binary_format); + let llvm_ar_archive = create_archive_with_llvm_ar( + &tmpdir, + archive_kind, + input_objects + .iter() + .map(|(name, bytes)| (*name, bytes.as_slice())), + ); + let ar_archive_writer_archive = create_archive_with_ar_archive_writer( + &tmpdir, + archive_kind, + input_objects + .iter() + .map(|(name, bytes)| (*name, bytes.as_slice())), + ); + + assert_eq!( + llvm_ar_archive, ar_archive_writer_archive, + "Archives differ for architecture: {architecture:?}, binary_format: {binary_format:?}, archive_kind: {archive_kind:?}", + ); + } +} + +pub fn add_file_with_functions_to_object( + object: &mut Object, + file_name: &[u8], + func_names: &[&[u8]], +) { + object.add_file_symbol(file_name.to_vec()); + + let text = object.section_id(write::StandardSection::Text); + object.append_section_data(text, &[1; 30], 4); + + for func_name in func_names { + let offset = object.append_section_data(text, &[1; 30], 4); + + object.add_symbol(write::Symbol { + name: func_name.to_vec(), + value: offset, + size: 32, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: write::SymbolSection::Section(text), + flags: SymbolFlags::None, + }); + } +} diff --git a/tests/multiple_objects.rs b/tests/multiple_objects.rs new file mode 100644 index 0000000..980be97 --- /dev/null +++ b/tests/multiple_objects.rs @@ -0,0 +1,65 @@ +use object::{write, Architecture}; + +mod common; + +/// Tests creating an archive with multiple objects. +/// Note that `func_overlapping` exists in both objects - this is to test +/// deduplication of symbols in the symbol table (where supported). +#[test] +fn basic_multiple_objects() { + common::generate_archive_and_compare( + "basic_multiple_objects", + |architecture, endianness, binary_format| { + let mut object1 = write::Object::new(binary_format, architecture, endianness); + // FIXME: Need 64-bit Big AIX symbol table support to match llvm-ar. + if architecture != Architecture::PowerPc64 { + common::add_file_with_functions_to_object( + &mut object1, + b"file1.c", + &[b"func1", b"func2", b"func_overlapping"], + ); + } + + let mut object2 = write::Object::new(binary_format, architecture, endianness); + if architecture != Architecture::PowerPc64 { + common::add_file_with_functions_to_object( + &mut object2, + b"file2.c", + &[b"func3", b"func4", b"func_overlapping"], + ); + } + + vec![ + ("file1.o", object1.write().unwrap()), + ("file2.o", object2.write().unwrap()), + ] + }, + ); +} + +/// Tests creating an archive with multiple objects with the same name. +/// This is important for Mach), which uses the timestamp when in deterministic +/// mode to differentiate the two objects. +#[test] +fn multiple_objects_same_name() { + common::generate_archive_and_compare( + "multiple_objects_same_name", + |architecture, endianness, binary_format| { + let mut object1 = write::Object::new(binary_format, architecture, endianness); + // FIXME: Need 64-bit Big AIX symbol table support to match llvm-ar. + if architecture != Architecture::PowerPc64 { + common::add_file_with_functions_to_object(&mut object1, b"file1.c", &[b"func1"]); + } + + let mut object2 = write::Object::new(binary_format, architecture, endianness); + if architecture != Architecture::PowerPc64 { + common::add_file_with_functions_to_object(&mut object2, b"file2.c", &[b"func2"]); + } + + vec![ + ("1/file.o", object1.write().unwrap()), + ("2/file.o", object2.write().unwrap()), + ] + }, + ); +} diff --git a/tests/round_trip.rs b/tests/round_trip.rs index d9bd3ca..89e44f3 100644 --- a/tests/round_trip.rs +++ b/tests/round_trip.rs @@ -1,12 +1,7 @@ // Derived from object's round_trip.rs: // https://github.com/gimli-rs/object/blob/0.32.0/tests/round_trip/mod.rs -use std::io::Cursor; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::{env, fs}; - -use ar_archive_writer::{ArchiveKind, NewArchiveMember}; +use ar_archive_writer::ArchiveKind; use object::{read, write}; use object::{ Architecture, BinaryFormat, Endianness, RelocationEncoding, RelocationKind, SymbolFlags, @@ -14,29 +9,7 @@ use object::{ }; use pretty_assertions::assert_eq; -fn run_llvm_ar(object_path: &Path, archive_path: &Path, archive_kind: ArchiveKind) { - let ar_path = cargo_binutils::Tool::Ar.path().unwrap(); - - let format_arg = match archive_kind { - ArchiveKind::Gnu => "gnu", - ArchiveKind::Darwin => "darwin", - ArchiveKind::AixBig => "bigarchive", - _ => panic!("unsupported archive kind: {:?}", archive_kind), - }; - - let output = Command::new(ar_path) - .arg(format!("--format={}", format_arg)) - .arg("rcs") - .arg(archive_path) - .arg(object_path) - .output() - .unwrap(); - assert_eq!( - String::from_utf8_lossy(&output.stderr), - "", - "llvm-ar failed. archive: {archive_path:?}. input: {object_path:?}" - ); -} +mod common; fn round_trip_and_diff( test_name: &str, @@ -44,32 +17,15 @@ fn round_trip_and_diff( archive_kind: ArchiveKind, trim_output_bytes: usize, ) { + let tmpdir = common::create_tmp_dir(test_name); let input_bytes = object.write().unwrap(); // Create a new archive using ar_archive_writer. - let output_archive_bytes = { - let input_member = NewArchiveMember { - buf: Box::new(&input_bytes) as Box>, - get_symbols: ar_archive_writer::get_native_object_symbols, - member_name: "input.o".to_string(), - mtime: 0, - uid: 0, - gid: 0, - perms: 0o644, - }; - let mut output_bytes = Cursor::new(Vec::new()); - ar_archive_writer::write_archive_to_stream( - &mut output_bytes, - &[input_member], - true, - archive_kind, - true, - false, - ) - .unwrap(); - - output_bytes.into_inner() - }; + let output_archive_bytes = common::create_archive_with_ar_archive_writer( + &tmpdir, + archive_kind, + [("input.o", input_bytes.as_slice())], + ); // Read the archive and member using object and diff with original data. { @@ -90,33 +46,16 @@ fn round_trip_and_diff( } // Use llvm-ar to create the archive and diff with ar_archive_writer. - { - let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(test_name); - match fs::remove_dir_all(&tmpdir) { - Ok(_) => {} - Err(err) => { - if err.kind() != std::io::ErrorKind::NotFound { - panic!("Failed to delete directory: {:?}", tmpdir); - } - } - } - fs::create_dir_all(&tmpdir).unwrap(); - let input_file_path = tmpdir.join("input.o"); - let archive_file_path = tmpdir.join("output.a"); - let ar_archive_writer_file_path = tmpdir.join("output_ar_writer.a"); - - fs::write(&input_file_path, &input_bytes).unwrap(); - - run_llvm_ar(&input_file_path, &archive_file_path, archive_kind); - let output_llvm_ar_bytes = fs::read(archive_file_path).unwrap(); - - fs::write(&ar_archive_writer_file_path, &output_archive_bytes).unwrap(); - assert_eq!( - output_archive_bytes, output_llvm_ar_bytes, - "Comparing ar_archive_writer to llvm-ar. Test case: build {:?} for {:?}", - archive_kind, object - ); - } + let output_llvm_ar_bytes = common::create_archive_with_llvm_ar( + &tmpdir, + archive_kind, + [("input.o", input_bytes.as_slice())], + ); + assert_eq!( + output_archive_bytes, output_llvm_ar_bytes, + "Comparing ar_archive_writer to llvm-ar. Test case: build {:?} for {:?}", + archive_kind, object + ); } #[test]