diff --git a/src/validation.rs b/src/validation.rs index c9902fabb..aed3c5308 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -8,7 +8,7 @@ use { clap::ArgMatches, normalize_path::NormalizePath, object::{ - Architecture, Endianness, FileKind, Object, SectionIndex, SymbolScope, + Architecture, Endianness, FileKind, Object, ObjectSymbol, SectionIndex, SymbolScope, elf::{ DF_1_NOW, DF_BIND_NOW, DF_TEXTREL, DT_BIND_NOW, DT_FLAGS, DT_FLAGS_1, DT_TEXTREL, ET_DYN, ET_EXEC, FileHeader32, FileHeader64, PF_W, PF_X, PT_GNU_RELRO, PT_GNU_STACK, @@ -899,6 +899,15 @@ pub struct ValidationContext { /// Symbols exported from dynamic libpython library. pub libpython_exported_symbols: BTreeSet, + /// Python symbols defined in the dynamic libpython library. + libpython_defined_symbols: BTreeSet, + + /// Python symbols defined by object files advertised in PYTHON.json. + advertised_object_defined_symbols: BTreeSet, + + /// Strong undefined Python symbols referenced by advertised object files. + advertised_object_undefined_symbols: BTreeSet, + /// Undefined Mach-O symbols that are required / non-weak. pub macho_undefined_symbols_strong: RequiredSymbols, @@ -913,6 +922,12 @@ impl ValidationContext { self.seen_dylibs.extend(other.seen_dylibs); self.libpython_exported_symbols .extend(other.libpython_exported_symbols); + self.libpython_defined_symbols + .extend(other.libpython_defined_symbols); + self.advertised_object_defined_symbols + .extend(other.advertised_object_defined_symbols); + self.advertised_object_undefined_symbols + .extend(other.advertised_object_undefined_symbols); self.macho_undefined_symbols_strong .merge(other.macho_undefined_symbols_strong); self.macho_undefined_symbols_weak @@ -920,6 +935,75 @@ impl ValidationContext { } } +fn normalize_object_symbol<'a>(name: &'a str, triple: &str) -> &'a str { + let name = if triple.contains("-apple-darwin") { + name.strip_prefix('_').unwrap_or(name) + } else if triple == "i686-pc-windows-msvc" { + name.strip_prefix(['_', '@']).unwrap_or(name) + } else { + name + }; + + if triple == "i686-pc-windows-msvc" + && let Some((name, suffix)) = name.rsplit_once('@') + && !name.is_empty() + && !suffix.is_empty() + && suffix.bytes().all(|byte| byte.is_ascii_digit()) + { + return name; + } + + name +} + +fn is_python_symbol(name: &str) -> bool { + name.starts_with("Py") || name.starts_with("_Py") +} + +fn is_libpython_file(path: &Path, suffix: &str) -> bool { + path.file_name() + .map(|filename| { + let filename = filename.to_string_lossy(); + filename.starts_with("libpython") && filename.ends_with(suffix) + }) + .unwrap_or(false) +} + +fn validate_libpython_object_symbols( + context: &ValidationContext, + object_coverage_complete: bool, + libpython_definition_coverage_complete: bool, +) -> Vec { + if !object_coverage_complete { + return vec![]; + } + + // Linked libpython binaries can also contain linker-generated symbols and + // private symbols from statically linked dependencies. Restrict this check + // to the Python API namespace so those unrelated symbols do not require + // advertising dependency object files in PYTHON.json. + let mut required_symbols = context + .libpython_exported_symbols + .iter() + .filter(|symbol| is_python_symbol(symbol)) + .collect::>(); + + if libpython_definition_coverage_complete { + required_symbols.extend( + context + .advertised_object_undefined_symbols + .intersection(&context.libpython_defined_symbols) + .filter(|symbol| is_python_symbol(symbol)), + ); + } + + required_symbols + .into_iter() + .filter(|symbol| !context.advertised_object_defined_symbols.contains(*symbol)) + .map(|symbol| format!("libpython symbol {symbol} is not defined by an advertised object")) + .collect() +} + fn validate_elf>( context: &mut ValidationContext, json: &PythonJsonMain, @@ -938,6 +1022,7 @@ fn validate_elf>( .file_name() .map(|name| name.to_string_lossy().starts_with("python")) .unwrap_or(false); + let is_libpython = is_libpython_file(path, ".so.1.0"); let mut has_stack_protector_symbol = false; let mut has_fortify_symbol = false; @@ -1136,6 +1221,13 @@ fn validate_elf>( for (symbol_index, symbol) in symbols.enumerate() { let name = String::from_utf8_lossy(symbol.name(endian, strings)?); let is_undefined_symbol = symbol.is_undefined(endian); + if is_libpython + && !is_undefined_symbol + && !name.is_empty() + && is_python_symbol(&name) + { + context.libpython_defined_symbols.insert(name.to_string()); + } // Stack protector and fortify are compiler features rather than // ELF properties. Their symbols provide a useful heuristic that @@ -1212,17 +1304,12 @@ fn validate_elf>( )); } - if let Some(filename) = path.file_name() { - let filename = filename.to_string_lossy(); - - if filename.starts_with("libpython") - && filename.ends_with(".so.1.0") - && matches!(symbol.st_bind(), STB_GLOBAL | STB_WEAK) - && symbol.st_shndx(endian) != SHN_UNDEF - && symbol.st_visibility() == STV_DEFAULT - { - context.libpython_exported_symbols.insert(name.to_string()); - } + if is_libpython + && matches!(symbol.st_bind(), STB_GLOBAL | STB_WEAK) + && symbol.st_shndx(endian) != SHN_UNDEF + && symbol.st_visibility() == STV_DEFAULT + { + context.libpython_exported_symbols.insert(name.to_string()); } } } @@ -1348,6 +1435,7 @@ fn validate_macho>( let advertised_sdk_version = semver::Version::parse(&format!("{advertised_sdk_version}.0"))?; let endian = header.endian()?; + let is_libpython = is_libpython_file(path, ".dylib"); let wanted_cpu_type = match target_triple { "aarch64-apple-darwin" => object::macho::CPU_TYPE_ARM64, @@ -1446,6 +1534,7 @@ fn validate_macho>( for symbol in table.iter() { let name = symbol.name(endian, strings)?; let name = String::from_utf8(name.to_vec())?; + let search_name = normalize_object_symbol(&name, target_triple); if symbol.is_undefined() { undefined_symbols.push(MachOSymbol { @@ -1453,6 +1542,10 @@ fn validate_macho>( library_ordinal: symbol.library_ordinal(endian), weak: symbol.n_desc(endian) & (object::macho::N_WEAK_REF) != 0, }); + } else if is_libpython && is_python_symbol(search_name) { + context + .libpython_defined_symbols + .insert(search_name.to_string()); } // Ensure specific symbols in dynamic binaries have proper visibility. @@ -1470,12 +1563,6 @@ fn validate_macho>( SymbolScope::Dynamic }; - let search_name = if let Some(v) = name.strip_prefix('_') { - v - } else { - name.as_str() - }; - if DEPENDENCY_PACKAGE_SYMBOLS.contains(&search_name) && scope == SymbolScope::Dynamic { @@ -1486,17 +1573,10 @@ fn validate_macho>( )); } - if let Some(filename) = path.file_name() { - let filename = filename.to_string_lossy(); - - if filename.starts_with("libpython") - && filename.ends_with(".dylib") - && scope == SymbolScope::Dynamic - { - context - .libpython_exported_symbols - .insert(search_name.to_string()); - } + if is_libpython && scope == SymbolScope::Dynamic { + context + .libpython_exported_symbols + .insert(search_name.to_string()); } } } @@ -1635,9 +1715,8 @@ fn validate_pe<'data, Pe: ImageNtHeaders>( if filename.starts_with("python") && filename.ends_with(".dll") { for symbol in pe.exports()? { - context - .libpython_exported_symbols - .insert(String::from_utf8(symbol.name().to_vec())?); + let symbol = String::from_utf8(symbol.name().to_vec())?; + context.libpython_exported_symbols.insert(symbol); } } @@ -1738,6 +1817,244 @@ fn validate_possible_object_file( Ok(context) } +fn collect_advertised_object_symbols( + context: &mut ValidationContext, + triple: &str, + declared_format: &str, + path: &Path, + data: &[u8], +) -> bool { + // LLVM bitcode objects used by LTO builds are not supported by the object + // crate. Only grant that exemption when both the metadata and bytes identify + // bitcode. LTO builds can also advertise native assembly objects, so dispatch + // those by their actual format while keeping overall symbol coverage partial. + let bitcode_declared = is_declared_llvm_bitcode_format(declared_format); + if bitcode_declared && has_recognizable_llvm_bitcode_container(data) { + return false; + } + let expected_format = if bitcode_declared { + target_object_format(triple).unwrap_or(declared_format) + } else { + declared_format + }; + if expected_format == "coff" && is_structurally_valid_msvc_ltcg_object(data, triple) { + return false; + } + + let Ok(kind) = FileKind::parse(data) else { + let expected = if bitcode_declared { + format!("LLVM bitcode or {expected_format}") + } else { + expected_format.to_string() + }; + context.errors.push(format!( + "advertised object {} could not be parsed as {expected}", + path.display() + )); + return false; + }; + if !object_kind_matches_declared_format(kind, expected_format) { + context.errors.push(format!( + "advertised object {} is {kind:?}, not the expected {expected_format} format", + path.display() + )); + return false; + } + + let Ok(file) = object::File::parse(data) else { + context.errors.push(format!( + "advertised object {} could not be parsed as {expected_format}", + path.display() + )); + return false; + }; + if file.kind() != object::ObjectKind::Relocatable { + context.errors.push(format!( + "advertised object {} is {:?}, not a relocatable object", + path.display(), + file.kind() + )); + return false; + } + if let Some(expected) = expected_object_architecture(triple) + && file.architecture() != expected + { + context.errors.push(format!( + "advertised object {} has architecture {:?}, expected {expected:?} for {triple}", + path.display(), + file.architecture() + )); + return false; + } + if let Some(expected) = expected_object_endianness(triple) + && file.endianness() != expected + { + context.errors.push(format!( + "advertised object {} has endianness {:?}, expected {expected:?} for {triple}", + path.display(), + file.endianness() + )); + return false; + } + + for symbol in file.symbols() { + if symbol.is_global() + && let Ok(name) = symbol.name() + { + let name = normalize_object_symbol(name, triple); + if !is_python_symbol(name) { + continue; + } + + if symbol.is_common() + || (!symbol.is_undefined() + && !matches!( + symbol.kind(), + object::SymbolKind::Section | object::SymbolKind::File + )) + { + context + .advertised_object_defined_symbols + .insert(name.to_string()); + } else if symbol.is_undefined() && !symbol.is_weak() { + context + .advertised_object_undefined_symbols + .insert(name.to_string()); + } + } + } + + !bitcode_declared +} + +fn is_structurally_valid_msvc_ltcg_object(data: &[u8], triple: &str) -> bool { + const HEADER_SIZE: usize = 32; + const CLASS_ID: [u8; 16] = [ + 0x38, 0xfe, 0xb3, 0x0c, 0xa5, 0xd9, 0xab, 0x4d, 0xac, 0x9b, 0xd6, 0xb6, 0x22, 0x26, 0x53, + 0xc2, + ]; + + let Some(header) = data.get(..HEADER_SIZE) else { + return false; + }; + let Some(expected_machine) = expected_coff_machine(triple) else { + return false; + }; + let version = u16::from_le_bytes([header[4], header[5]]); + let machine = u16::from_le_bytes([header[6], header[7]]); + let payload_size = + u32::from_le_bytes(header[28..32].try_into().expect("fixed-size header")) as usize; + + header[..4] == [0x00, 0x00, 0xff, 0xff] + && version == 1 + && machine == expected_machine + && header[12..28] == CLASS_ID + && payload_size > 0 + && HEADER_SIZE.checked_add(payload_size) == Some(data.len()) +} + +fn expected_coff_machine(triple: &str) -> Option { + match triple { + "aarch64-pc-windows-msvc" => Some(object::pe::IMAGE_FILE_MACHINE_ARM64), + "i686-pc-windows-msvc" => Some(object::pe::IMAGE_FILE_MACHINE_I386), + "x86_64-pc-windows-msvc" => Some(object::pe::IMAGE_FILE_MACHINE_AMD64), + _ => None, + } +} + +fn has_recognizable_llvm_bitcode_container(data: &[u8]) -> bool { + const RAW_MAGIC: &[u8] = &[0x42, 0x43, 0xc0, 0xde]; + const WRAPPER_MAGIC: &[u8] = &[0xde, 0xc0, 0x17, 0x0b]; + + // The object crate cannot parse LLVM bitcode. Recognize the container and + // reject truncated magic or an invalid wrapper, but do not claim to validate + // the embedded bitstream. Symbol coverage remains unavailable for bitcode. + if data.starts_with(RAW_MAGIC) { + return data.len() >= 16; + } + if !data.starts_with(WRAPPER_MAGIC) || data.len() < 16 { + return false; + } + + let offset = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize; + let size = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize; + let Some(end) = offset.checked_add(size) else { + return false; + }; + + size >= 16 + && data + .get(offset..end) + .map(|payload| payload.starts_with(RAW_MAGIC)) + .unwrap_or(false) +} + +fn is_declared_llvm_bitcode_format(format: &str) -> bool { + format + .strip_prefix("llvm-bitcode:") + .map(|version| !version.is_empty()) + .unwrap_or(false) +} + +fn object_kind_matches_declared_format(kind: FileKind, declared_format: &str) -> bool { + match declared_format { + "elf" => matches!(kind, FileKind::Elf32 | FileKind::Elf64), + "mach-o" => matches!(kind, FileKind::MachO32 | FileKind::MachO64), + "coff" => matches!(kind, FileKind::Coff | FileKind::CoffBig), + _ => false, + } +} + +fn target_object_format(triple: &str) -> Option<&'static str> { + if triple.contains("-apple-darwin") { + Some("mach-o") + } else if triple.contains("-pc-windows-") { + Some("coff") + } else if triple.contains("-unknown-linux-") { + Some("elf") + } else { + None + } +} + +fn declared_native_object_format_matches_target(triple: &str, declared_format: &str) -> bool { + target_object_format(triple) == Some(declared_format) +} + +fn expected_object_architecture(triple: &str) -> Option { + if triple.starts_with("aarch64-") { + Some(Architecture::Aarch64) + } else if triple.starts_with("armv7-") { + Some(Architecture::Arm) + } else if triple.starts_with("i686-") { + Some(Architecture::I386) + } else if triple.starts_with("mips64") { + Some(Architecture::Mips64) + } else if triple.starts_with("mips") { + Some(Architecture::Mips) + } else if triple.starts_with("ppc64") { + Some(Architecture::PowerPc64) + } else if triple.starts_with("riscv64-") { + Some(Architecture::Riscv64) + } else if triple.starts_with("s390x-") { + Some(Architecture::S390x) + } else if triple.starts_with("x86_64") { + Some(Architecture::X86_64) + } else { + None + } +} + +fn expected_object_endianness(triple: &str) -> Option { + if triple.starts_with("mips-") || triple.starts_with("s390x-") { + Some(Endianness::Big) + } else if RECOGNIZED_TRIPLES.contains(&triple) { + Some(Endianness::Little) + } else { + None + } +} + fn validate_extension_modules( python_major_minor: &str, target_triple: &str, @@ -2008,6 +2325,38 @@ fn validate_distribution( )); } + if json.is_none() { + return Ok(context.errors); + } + + let advertised_object_paths = json + .as_ref() + .map(|json| { + json.all_object_paths() + .into_iter() + .map(|path| PathBuf::from("python").join(path)) + .collect::>() + }) + .unwrap_or_default(); + let object_file_format = json.as_ref().unwrap().build_info.object_file_format.clone(); + let supports_object_symbol_validation = + matches!(object_file_format.as_str(), "coff" | "elf" | "mach-o"); + let is_llvm_bitcode_format = is_declared_llvm_bitcode_format(&object_file_format); + let native_object_format_matches_target = !supports_object_symbol_validation + || declared_native_object_format_matches_target(triple, &object_file_format); + if !supports_object_symbol_validation && !is_llvm_bitcode_format { + context.errors.push(format!( + "PYTHON.json declares unsupported object_file_format {object_file_format}" + )); + } + if supports_object_symbol_validation && !native_object_format_matches_target { + context.errors.push(format!( + "PYTHON.json declares object_file_format {object_file_format}, but {triple} requires {}", + target_object_format(triple).unwrap_or("its native object format") + )); + } + let mut advertised_object_symbol_coverage_complete = true; + let mut bin_python = None; let mut bin_python3 = None; @@ -2045,6 +2394,17 @@ fn validate_distribution( &path, &data, )?); + if (supports_object_symbol_validation || is_llvm_bitcode_format) + && advertised_object_paths.contains(&path) + { + advertised_object_symbol_coverage_complete &= collect_advertised_object_symbols( + &mut context, + triple, + &object_file_format, + &path, + &data, + ); + } // Descend into archive files (static libraries are archive files and members // are usually object files). @@ -2290,17 +2650,24 @@ fn validate_distribution( } // Ensure all referenced object paths are in the archive. - for object_path in json.as_ref().unwrap().all_object_paths() { - let wanted_path = PathBuf::from("python").join(object_path); - - if !seen_paths.contains(&wanted_path) { - context.errors.push(format!( - "PYTHON.json referenced object file not in tar archive: {}", - wanted_path.display() - )); - } + for wanted_path in advertised_object_paths.difference(&seen_paths) { + context.errors.push(format!( + "PYTHON.json referenced object file not in tar archive: {}", + wanted_path.display() + )); } + let object_symbol_coverage_complete = !is_static + && supports_object_symbol_validation + && native_object_format_matches_target + && advertised_object_symbol_coverage_complete + && advertised_object_paths.is_subset(&seen_paths); + context.errors.extend(validate_libpython_object_symbols( + &context, + object_symbol_coverage_complete, + object_symbol_coverage_complete && object_file_format != "coff", + )); + Ok(context.errors) } @@ -2334,3 +2701,249 @@ pub fn command_validate_distribution(args: &ArgMatches) -> Result<()> { Err(anyhow!("errors found")) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalizes_platform_object_symbols() { + assert_eq!( + normalize_object_symbol("_PyToken_Init", "x86_64-unknown-linux-gnu"), + "_PyToken_Init" + ); + assert_eq!( + normalize_object_symbol("__PyToken_Init", "aarch64-apple-darwin"), + "_PyToken_Init" + ); + assert_eq!( + normalize_object_symbol("_PyLong_FromLong", "i686-pc-windows-msvc"), + "PyLong_FromLong" + ); + assert_eq!( + normalize_object_symbol("__PyToken_Init", "i686-pc-windows-msvc"), + "_PyToken_Init" + ); + assert_eq!( + normalize_object_symbol("_PyArg_ParseTuple@8", "i686-pc-windows-msvc"), + "PyArg_ParseTuple" + ); + assert_eq!( + normalize_object_symbol("@PyArg_ParseTuple@8", "i686-pc-windows-msvc"), + "PyArg_ParseTuple" + ); + } + + #[test] + fn reports_missing_symbols_deterministically() { + let exported = BTreeSet::from([ + "PyLong_FromLong".to_string(), + "PyMissingExport".to_string(), + "_init".to_string(), + ]); + let libpython_defined = BTreeSet::from([ + "PyLong_FromLong".to_string(), + "_PyToken_Init".to_string(), + "_PyTokenizer_Get".to_string(), + "sqlite3_open".to_string(), + ]); + let object_defined = + BTreeSet::from(["PyLong_FromLong".to_string(), "_Py_tss_tstate".to_string()]); + let object_undefined = BTreeSet::from([ + "_PyTokenizer_Get".to_string(), + "_PyToken_Init".to_string(), + "external_system_symbol".to_string(), + "sqlite3_open".to_string(), + ]); + let context = ValidationContext { + libpython_exported_symbols: exported, + libpython_defined_symbols: libpython_defined, + advertised_object_defined_symbols: object_defined, + advertised_object_undefined_symbols: object_undefined, + ..ValidationContext::default() + }; + + assert_eq!( + validate_libpython_object_symbols(&context, true, true), + vec![ + "libpython symbol PyMissingExport is not defined by an advertised object", + "libpython symbol _PyToken_Init is not defined by an advertised object", + "libpython symbol _PyTokenizer_Get is not defined by an advertised object", + ] + ); + } + + #[test] + fn skips_symbol_comparison_without_complete_object_coverage() { + let context = ValidationContext { + libpython_exported_symbols: BTreeSet::from(["_PyToken_Init".to_string()]), + ..ValidationContext::default() + }; + + assert!(validate_libpython_object_symbols(&context, false, false).is_empty()); + } + + #[test] + fn coff_checks_exports_without_claiming_hidden_symbol_coverage() { + let context = ValidationContext { + libpython_exported_symbols: BTreeSet::from(["PyMissingExport".to_string()]), + libpython_defined_symbols: BTreeSet::from(["_PyHidden".to_string()]), + advertised_object_undefined_symbols: BTreeSet::from(["_PyHidden".to_string()]), + ..ValidationContext::default() + }; + + assert_eq!( + validate_libpython_object_symbols(&context, true, false), + vec!["libpython symbol PyMissingExport is not defined by an advertised object"] + ); + } + + #[test] + fn accepts_msvc_ltcg_object_as_partial_coverage() { + let mut data = [ + 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x64, 0x86, 0x00, 0x00, 0x00, 0x00, 0x38, 0xfe, + 0xb3, 0x0c, 0xa5, 0xd9, 0xab, 0x4d, 0xac, 0x9b, 0xd6, 0xb6, 0x22, 0x26, 0x53, 0xc2, + 0x04, 0x00, 0x00, 0x00, 0x13, 0x0c, 0x07, 0x00, + ]; + let mut context = ValidationContext::default(); + + assert!(!collect_advertised_object_symbols( + &mut context, + "x86_64-pc-windows-msvc", + "coff", + Path::new("python/build/core/Python-ast.obj"), + &data, + )); + assert!(context.errors.is_empty(), "{:?}", context.errors); + + for index in [0, 4, 6, 12, 28] { + data[index] ^= 1; + let mut malformed_context = ValidationContext::default(); + assert!(!collect_advertised_object_symbols( + &mut malformed_context, + "x86_64-pc-windows-msvc", + "coff", + Path::new("python/build/core/Python-ast.obj"), + &data, + )); + assert_eq!(malformed_context.errors.len(), 1); + data[index] ^= 1; + } + + for (triple, malformed_data) in [ + ("x86_64-pc-windows-msvc", &data[..31]), + ("aarch64-pc-windows-msvc", &data[..]), + ] { + let mut malformed_context = ValidationContext::default(); + assert!(!collect_advertised_object_symbols( + &mut malformed_context, + triple, + "coff", + Path::new("python/build/core/Python-ast.obj"), + malformed_data, + )); + assert_eq!(malformed_context.errors.len(), 1); + } + } + + #[test] + fn merge_preserves_collected_symbol_sets() { + let mut context = ValidationContext { + libpython_defined_symbols: BTreeSet::from(["_PyDefinedA".to_string()]), + advertised_object_defined_symbols: BTreeSet::from(["_PyObjectA".to_string()]), + advertised_object_undefined_symbols: BTreeSet::from(["_PyUndefinedA".to_string()]), + ..ValidationContext::default() + }; + context.merge(ValidationContext { + libpython_defined_symbols: BTreeSet::from(["_PyDefinedB".to_string()]), + advertised_object_defined_symbols: BTreeSet::from(["_PyObjectB".to_string()]), + advertised_object_undefined_symbols: BTreeSet::from(["_PyUndefinedB".to_string()]), + ..ValidationContext::default() + }); + + assert_eq!( + context.libpython_defined_symbols, + BTreeSet::from(["_PyDefinedA".to_string(), "_PyDefinedB".to_string()]) + ); + assert_eq!( + context.advertised_object_defined_symbols, + BTreeSet::from(["_PyObjectA".to_string(), "_PyObjectB".to_string()]) + ); + assert_eq!( + context.advertised_object_undefined_symbols, + BTreeSet::from(["_PyUndefinedA".to_string(), "_PyUndefinedB".to_string()]) + ); + } + + #[test] + fn recognizes_llvm_bitcode_container_structure() { + assert!(!has_recognizable_llvm_bitcode_container(&[ + 0x42, 0x43, 0xc0, 0xde + ])); + assert!(has_recognizable_llvm_bitcode_container(&[ + 0x42, 0x43, 0xc0, 0xde, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00 + ])); + + let mut wrapped = vec![0xde, 0xc0, 0x17, 0x0b, 0, 0, 0, 0]; + wrapped.extend_from_slice(&16_u32.to_le_bytes()); + wrapped.extend_from_slice(&16_u32.to_le_bytes()); + wrapped.extend_from_slice(&[ + 0x42, 0x43, 0xc0, 0xde, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, + ]); + assert!(has_recognizable_llvm_bitcode_container(&wrapped)); + + wrapped[12..16].copy_from_slice(&50_u32.to_le_bytes()); + assert!(!has_recognizable_llvm_bitcode_container(&wrapped)); + assert!(!has_recognizable_llvm_bitcode_container(b"not bitcode")); + } + + #[test] + fn maps_supported_target_architectures() { + assert_eq!( + expected_object_architecture("aarch64-pc-windows-msvc"), + Some(Architecture::Aarch64) + ); + assert_eq!( + expected_object_architecture("i686-pc-windows-msvc"), + Some(Architecture::I386) + ); + assert_eq!( + expected_object_architecture("mips64el-unknown-linux-gnuabi64"), + Some(Architecture::Mips64) + ); + assert_eq!( + expected_object_architecture("x86_64_v3-unknown-linux-gnu"), + Some(Architecture::X86_64) + ); + assert_eq!( + expected_object_endianness("mips-unknown-linux-gnu"), + Some(Endianness::Big) + ); + assert_eq!( + expected_object_endianness("mipsel-unknown-linux-gnu"), + Some(Endianness::Little) + ); + assert_eq!( + expected_object_endianness("s390x-unknown-linux-gnu"), + Some(Endianness::Big) + ); + assert_eq!( + target_object_format("x86_64_v2-unknown-linux-gnu"), + Some("elf") + ); + assert_eq!( + target_object_format("aarch64-pc-windows-msvc"), + Some("coff") + ); + assert!(declared_native_object_format_matches_target( + "x86_64-unknown-linux-gnu", + "elf" + )); + assert!(!declared_native_object_format_matches_target( + "x86_64-unknown-linux-gnu", + "coff" + )); + } +}