diff --git a/Cargo.toml b/Cargo.toml index acfaf3e..f9443f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,17 +11,17 @@ description = "elf-cam is a WebAssembly(WASM) module to extract very specific in crate-type = ["cdylib", "rlib"] [features] -default = ["console_error_panic_hook"] +default = ["console_error_panic_hook", "wee_alloc"] [dependencies] -wasm-bindgen = "0.2.63" -goblin = { version = "0.2", default-features = false, features = ["alloc", "elf32", "elf64", "endian_fd"] } +wasm-bindgen = "0.2.81" +goblin = { version = "0.5.2", default-features = false, features = ["archive", "std", "elf32", "elf64", "mach32", "mach64", "pe32", "pe64"] } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # code size when deploying. -console_error_panic_hook = { version = "0.1.6", optional = true } +console_error_panic_hook = { version = "0.1.7", optional = true } # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size # compared to the default allocator's ~10K. It is slower than the default diff --git a/src/elf.rs b/src/elf.rs deleted file mode 100644 index 584e144..0000000 --- a/src/elf.rs +++ /dev/null @@ -1,77 +0,0 @@ -use goblin::{elf::sym, elf::Elf, error::Error}; - -const RUST_PERSONALITY: &str = "rust_eh_personality"; -const GO_SECTION: &str = ".note.go.buildid"; - -#[derive(Debug, PartialEq)] -pub enum Runtime { - Go, - Rust, -} - -pub fn detect(data: &[u8]) -> Result, Error> { - let elf = match Elf::parse(data) { - Ok(elf) => elf, - _ => return Ok(None), - }; - - for s in elf.shdr_strtab.to_vec()? { - if s == GO_SECTION { - return Ok(Some(Runtime::Go)); - } - } - - for s in elf.strtab.to_vec()? { - if s == RUST_PERSONALITY { - return Ok(Some(Runtime::Rust)); - } - } - - for s in elf.syms.iter() { - if s.is_function() && s.st_bind() == sym::STB_GLOBAL { - if let Some(Ok(sym_name)) = elf.strtab.get(s.st_name) { - if sym_name == RUST_PERSONALITY { - return Ok(Some(Runtime::Rust)); - } - } - } - } - - Ok(None) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_detect_go_runtime() { - let buffer = - std::fs::read("tests/data/hello-world-go").expect("failed to load binary file"); - - let runtime = detect(&buffer) - .expect("failed to detect runtime") - .expect("failed to return some runtime"); - assert_eq!(Runtime::Go, runtime); - } - - #[test] - fn test_detect_rust_runtime() { - let buffer = - std::fs::read("tests/data/hello-world-rs").expect("failed to load binary file"); - - let runtime = detect(&buffer) - .expect("failed to detect runtime") - .expect("failed to return some runtime"); - assert_eq!(Runtime::Rust, runtime); - } - - #[test] - fn test_detect_ignores_invalid_file() { - let buffer = - std::fs::read("tests/data/hello-world-text").expect("failed to load binary file"); - - let runtime = detect(&buffer).expect("failed to detect runtime"); - assert_eq!(None, runtime); - } -} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ea8c026 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,33 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub struct InfoError { + details: String, +} + +impl InfoError { + pub fn new(msg: impl ToString) -> InfoError { + InfoError { + details: msg.to_string(), + } + } +} + +impl fmt::Display for InfoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.details) + } +} + +impl Error for InfoError { + fn description(&self) -> &str { + &self.details + } +} + +impl From for InfoError { + fn from(err: goblin::error::Error) -> Self { + InfoError::new(err) + } +} diff --git a/src/info.rs b/src/info.rs new file mode 100644 index 0000000..cd4408e --- /dev/null +++ b/src/info.rs @@ -0,0 +1,249 @@ +use crate::error::InfoError; +use goblin::{ + elf::sym, + elf::Elf, + elf64::header::{EM_386, EM_AARCH64, EM_ARM, EM_X86_64}, + mach::{ + cputype::{CPU_TYPE_ARM64, CPU_TYPE_X86_64}, + Mach, + }, + pe::header::{COFF_MACHINE_ARM64, COFF_MACHINE_X86, COFF_MACHINE_X86_64}, + Object as Obj, +}; +use wasm_bindgen::prelude::*; + +const RUST_PERSONALITY: &str = "rust_eh_personality"; +const GO_SECTION: &str = ".note.go.buildid"; + +#[wasm_bindgen] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Runtime { + Go, + Rust, +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Platform { + Win32, + Darwin, + Linux, +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Arch { + X86, + Amd64, + Arm, + Arm64, +} + +#[wasm_bindgen] +pub struct BinaryInfo { + pub platform: Platform, + pub arch: Arch, + pub runtime: Option, +} + +pub fn get_runtime_from_elf(elf: Elf) -> Result, goblin::error::Error> { + for s in elf.shdr_strtab.to_vec()? { + if s == GO_SECTION { + return Ok(Some(Runtime::Go)); + } + } + + for s in elf.strtab.to_vec()? { + if s == RUST_PERSONALITY { + return Ok(Some(Runtime::Rust)); + } + } + + for s in elf.syms.iter() { + if s.is_function() && s.st_bind() == sym::STB_GLOBAL { + if let Some(sym_name) = elf.strtab.get_at(s.st_name) { + if sym_name == RUST_PERSONALITY { + return Ok(Some(Runtime::Rust)); + } + } + } + } + + Ok(None) +} + +// Implementation initially based on timfish/binary-info +// https://github.com/timfish/binary-info/blob/v0.0.3/LICENSE +pub fn get_info(buffer: &[u8]) -> Result { + match Obj::parse(buffer)? { + Obj::Elf(elf) => { + let arch = match elf.header.e_machine { + EM_AARCH64 => Arch::Arm64, + EM_X86_64 => Arch::Amd64, + EM_ARM => Arch::Arm, + EM_386 => Arch::X86, + _ => return Err(InfoError::new("Unknown architecture")), + }; + + let runtime = get_runtime_from_elf(elf)?; + + Ok(BinaryInfo { + platform: Platform::Linux, + arch, + runtime, + }) + } + Obj::PE(pe) => { + let arch = match pe.header.coff_header.machine { + COFF_MACHINE_ARM64 => Arch::Arm64, + COFF_MACHINE_X86 => Arch::X86, + COFF_MACHINE_X86_64 => Arch::Amd64, + _ => return Err(InfoError::new("Unknown architecture")), + }; + + Ok(BinaryInfo { + platform: Platform::Win32, + arch: arch, + runtime: None, + }) + } + Obj::Mach(mach) => match mach { + Mach::Fat(_) => return Err(InfoError::new("Unsupported binary")), + Mach::Binary(mach_o) => { + let arch = match mach_o.header.cputype() { + CPU_TYPE_X86_64 => Arch::Amd64, + CPU_TYPE_ARM64 => Arch::Arm64, + _ => return Err(InfoError::new("Unknown architecture")), + }; + + Ok(BinaryInfo { + platform: Platform::Darwin, + arch: arch, + runtime: None, + }) + } + }, + _ => Err(InfoError::new("Not a binary")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_detect_go_runtime_darwin_amd64() { + let buffer = + std::fs::read("tests/data/darwin/go-amd64").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Amd64, info.arch); + assert_eq!(Platform::Darwin, info.platform); + assert!(info.runtime.is_none()) + } + + #[test] + fn test_detect_go_runtime_darwin_arm64() { + let buffer = + std::fs::read("tests/data/darwin/go-arm64").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Arm64, info.arch); + assert_eq!(Platform::Darwin, info.platform); + assert!(info.runtime.is_none()) + } + + #[test] + fn test_detect_go_runtime_linux_x86() { + let buffer = std::fs::read("tests/data/linux/go-x86").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::X86, info.arch); + assert_eq!(Platform::Linux, info.platform); + assert_eq!(Runtime::Go, info.runtime.unwrap()); + } + + #[test] + fn test_detect_go_runtime_linux_amd64() { + let buffer = + std::fs::read("tests/data/linux/go-amd64").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Amd64, info.arch); + assert_eq!(Platform::Linux, info.platform); + assert_eq!(Runtime::Go, info.runtime.unwrap()); + } + + #[test] + fn test_detect_go_runtime_linux_arm() { + let buffer = std::fs::read("tests/data/linux/go-arm").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Arm, info.arch); + assert_eq!(Platform::Linux, info.platform); + assert_eq!(Runtime::Go, info.runtime.unwrap()); + } + + #[test] + fn test_detect_go_runtime_linux_arm64() { + let buffer = + std::fs::read("tests/data/linux/go-arm64").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Arm64, info.arch); + assert_eq!(Platform::Linux, info.platform); + assert_eq!(Runtime::Go, info.runtime.unwrap()); + } + + #[test] + fn test_detect_rust_runtime_darwin_amd64() { + let buffer = + std::fs::read("tests/data/darwin/rust-amd64").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Amd64, info.arch); + assert_eq!(Platform::Darwin, info.platform); + assert!(info.runtime.is_none()) + } + + #[test] + fn test_detect_rust_runtime_linux_amd64() { + let buffer = + std::fs::read("tests/data/linux/rust-amd64").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Amd64, info.arch); + assert_eq!(Platform::Linux, info.platform); + assert_eq!(Runtime::Rust, info.runtime.unwrap()); + } + + #[test] + fn test_detect_go_runtime_windows_amd64() { + let buffer = + std::fs::read("tests/data/windows/go-amd64.exe").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::Amd64, info.arch); + assert_eq!(Platform::Win32, info.platform); + assert!(info.runtime.is_none()) + } + + #[test] + fn test_detect_go_runtime_windows_x86() { + let buffer = + std::fs::read("tests/data/windows/go-x86.exe").expect("failed to load binary file"); + + let info = get_info(&buffer).expect("failed to detect runtime"); + assert_eq!(Arch::X86, info.arch); + assert_eq!(Platform::Win32, info.platform); + assert!(info.runtime.is_none()) + } + + #[test] + #[should_panic] + fn test_detect_ignores_invalid_file() { + let buffer = std::fs::read("tests/data/text").expect("failed to load binary file"); + + get_info(&buffer).expect("failed to detect runtime"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3fc6979..b5063af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -mod elf; +mod error; +mod info; use wasm_bindgen::prelude::*; @@ -8,27 +9,10 @@ use wasm_bindgen::prelude::*; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -#[wasm_bindgen] -pub enum Runtime { - Go, - Rust, -} - -impl From for Runtime { - fn from(rt: elf::Runtime) -> Self { - match rt { - elf::Runtime::Go => Runtime::Go, - elf::Runtime::Rust => Runtime::Rust, - } - } -} - #[wasm_bindgen(catch)] -pub fn detect(data: &[u8]) -> Result, JsValue> { +pub fn detect(data: &[u8]) -> Result { set_panic_hook(); - elf::detect(data) - .map_err(|e| JsValue::from(format!("error reading elf metadata: {}", e))) - .map(|o| o.map(Runtime::from)) + info::get_info(data).map_err(|e| JsValue::from(format!("error reading binary: {}", e))) } fn set_panic_hook() { diff --git a/tests/data/build_data.sh b/tests/data/build_data.sh new file mode 100755 index 0000000..66e1f56 --- /dev/null +++ b/tests/data/build_data.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# GO +# Linux +GOOS="linux" GOARCH="386" go build -ldflags "-s -w" -o ./linux/go-x86 ./src/go/hello-world.go +GOOS="linux" GOARCH="amd64" go build -ldflags "-s -w" -o ./linux/go-amd64 ./src/go/hello-world.go +GOOS="linux" GOARCH="arm" go build -ldflags "-s -w" -o ./linux/go-arm ./src/go/hello-world.go +GOOS="linux" GOARCH="arm64" go build -ldflags "-s -w" -o ./linux/go-arm64 ./src/go/hello-world.go +# Darwin +GOOS="darwin" GOARCH="amd64" go build -ldflags "-s -w" -o ./darwin/go-amd64 ./src/go/hello-world.go +GOOS="darwin" GOARCH="arm64" go build -ldflags "-s -w" -o ./darwin/go-arm64 ./src/go/hello-world.go +# Windows +GOOS="windows" GOARCH="amd64" go build -ldflags "-s -w" -o ./windows/go-amd64.exe ./src/go/hello-world.go +GOOS="windows" GOARCH="386" go build -ldflags "-s -w" -o ./windows/go-x86.exe ./src/go/hello-world.go + + +# For rust there is more setup needed, install a linker for the platform etc, +# so we are not doing it in this script. could use docker in theory diff --git a/tests/data/darwin/go-amd64 b/tests/data/darwin/go-amd64 new file mode 100755 index 0000000..0b1fc74 Binary files /dev/null and b/tests/data/darwin/go-amd64 differ diff --git a/tests/data/darwin/go-arm64 b/tests/data/darwin/go-arm64 new file mode 100755 index 0000000..91de1cd Binary files /dev/null and b/tests/data/darwin/go-arm64 differ diff --git a/tests/data/darwin/rust-amd64 b/tests/data/darwin/rust-amd64 new file mode 100755 index 0000000..9fdcbb8 Binary files /dev/null and b/tests/data/darwin/rust-amd64 differ diff --git a/tests/data/hello-world-go b/tests/data/hello-world-go deleted file mode 100755 index 716c5b3..0000000 Binary files a/tests/data/hello-world-go and /dev/null differ diff --git a/tests/data/linux/go-amd64 b/tests/data/linux/go-amd64 new file mode 100755 index 0000000..934ffba Binary files /dev/null and b/tests/data/linux/go-amd64 differ diff --git a/tests/data/linux/go-arm b/tests/data/linux/go-arm new file mode 100755 index 0000000..c96df0a Binary files /dev/null and b/tests/data/linux/go-arm differ diff --git a/tests/data/linux/go-arm64 b/tests/data/linux/go-arm64 new file mode 100755 index 0000000..ec225dc Binary files /dev/null and b/tests/data/linux/go-arm64 differ diff --git a/tests/data/linux/go-x86 b/tests/data/linux/go-x86 new file mode 100755 index 0000000..6817936 Binary files /dev/null and b/tests/data/linux/go-x86 differ diff --git a/tests/data/hello-world-rs b/tests/data/linux/rust-amd64 similarity index 100% rename from tests/data/hello-world-rs rename to tests/data/linux/rust-amd64 diff --git a/tests/data/src/go/hello-world.go b/tests/data/src/go/hello-world.go new file mode 100644 index 0000000..f2482c1 --- /dev/null +++ b/tests/data/src/go/hello-world.go @@ -0,0 +1,5 @@ +package main +import "fmt" +func main() { + fmt.Println("hello world") +} diff --git a/tests/data/src/rust/hello-world.rs b/tests/data/src/rust/hello-world.rs new file mode 100644 index 0000000..47ad8c6 --- /dev/null +++ b/tests/data/src/rust/hello-world.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello World!"); +} diff --git a/tests/data/windows/go-amd64.exe b/tests/data/windows/go-amd64.exe new file mode 100755 index 0000000..98e6fa5 Binary files /dev/null and b/tests/data/windows/go-amd64.exe differ diff --git a/tests/data/windows/go-x86.exe b/tests/data/windows/go-x86.exe new file mode 100755 index 0000000..2654835 Binary files /dev/null and b/tests/data/windows/go-x86.exe differ