diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/rust/embed-manifest/src/embed/coff.rs | 192 | ||||
-rw-r--r-- | third_party/rust/embed-manifest/src/embed/error.rs | 57 | ||||
-rw-r--r-- | third_party/rust/embed-manifest/src/embed/mod.rs | 139 | ||||
-rw-r--r-- | third_party/rust/embed-manifest/src/embed/test.rs | 173 |
4 files changed, 561 insertions, 0 deletions
diff --git a/third_party/rust/embed-manifest/src/embed/coff.rs b/third_party/rust/embed-manifest/src/embed/coff.rs new file mode 100644 index 0000000000..bf751c3bf5 --- /dev/null +++ b/third_party/rust/embed-manifest/src/embed/coff.rs @@ -0,0 +1,192 @@ +//! Just as much COFF object file support as is needed to write a resource data segment +//! for GNU Windows targets. Inspired by the `write::Object` code from the `object` crate. +//! +//! Integers are converted from u64 to u32 and added without checking because the manifest +//! data cannot get anywhere close to overflowing unless the supplied application name or +//! number of dependencies was extremely long. If this code was used more generally or if +//! the input was less trustworthy then more checked conversions and checked arithmetic +//! would be needed. + +use std::io::{self, Seek, SeekFrom, Write}; +use std::time::SystemTime; + +#[derive(Debug, Clone, Copy)] +pub enum MachineType { + I386, + X86_64, + Aarch64, +} + +impl MachineType { + pub fn machine(&self) -> u16 { + match self { + Self::I386 => 0x014c, + Self::X86_64 => 0x8664, + Self::Aarch64 => 0xaa64, + } + } + + pub fn relocation_type(&self) -> u16 { + match self { + Self::I386 => 7, + Self::X86_64 => 3, + Self::Aarch64 => 2, + } + } +} + +pub struct CoffWriter<W> { + /// wrapped writer or buffer + writer: W, + /// machine type for file header + machine_type: MachineType, + // size in bytes of resource section data + size_of_raw_data: u32, + // number of relocations at the end of the section + number_of_relocations: u16, +} + +impl<W: Write + Seek> CoffWriter<W> { + /// Create a new instance wrapping a writer. + pub fn new(mut writer: W, machine_type: MachineType) -> io::Result<Self> { + // Add space for file header and section table. + writer.write_all(&[0; 60])?; + Ok(Self { + writer, + machine_type, + size_of_raw_data: 0, + number_of_relocations: 0, + }) + } + + /// Add data to a section and return its offset within the section. + pub fn add_data(&mut self, data: &[u8]) -> io::Result<u32> { + let start = self.size_of_raw_data; + self.writer.write_all(data)?; + self.size_of_raw_data = start + data.len() as u32; + Ok(start) + } + + // Pad the resource data to a multiple of `n` bytes. + pub fn align_to(&mut self, n: u32) -> io::Result<()> { + let offset = self.size_of_raw_data % n; + if offset != 0 { + let padding = n - offset; + for _ in 0..padding { + self.writer.write_all(&[0])?; + } + self.size_of_raw_data += padding; + } + Ok(()) + } + + /// Write a relocation for a symbol at the end of the section. + pub fn add_relocation(&mut self, address: u32) -> io::Result<()> { + self.number_of_relocations += 1; + self.writer.write_all(&address.to_le_bytes())?; + self.writer.write_all(&[0, 0, 0, 0])?; + self.writer.write_all(&self.machine_type.relocation_type().to_le_bytes()) + } + + /// Write the object and section headers and write the symbol table. + pub fn finish(mut self) -> io::Result<W> { + // Get the timestamp for the header. `as` is correct here, as the low 32 bits + // should be used. + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_or(0, |d| d.as_secs() as u32); + + // Copy file location of the symbol table. + let pointer_to_symbol_table = self.writer.stream_position()? as u32; + + // Write the symbols and auxiliary data for the section. + self.writer.write_all(b".rsrc\0\0\0")?; // name + self.writer.write_all(&[0, 0, 0, 0])?; // address + self.writer.write_all(&[1, 0])?; // section number (1-based) + self.writer.write_all(&[0, 0, 3, 1])?; // type = 0, class = static, aux symbols = 1 + self.writer.write_all(&self.size_of_raw_data.to_le_bytes())?; + self.writer.write_all(&self.number_of_relocations.to_le_bytes())?; + self.writer.write_all(&[0; 12])?; + + // Write the empty string table. + self.writer.write_all(&[0; 4])?; + + // Write the object header. + let end_of_file = self.writer.seek(SeekFrom::Start(0))?; + self.writer.write_all(&self.machine_type.machine().to_le_bytes())?; + self.writer.write_all(&[1, 0])?; // number of sections + self.writer.write_all(×tamp.to_le_bytes())?; + self.writer.write_all(&pointer_to_symbol_table.to_le_bytes())?; + self.writer.write_all(&[2, 0, 0, 0])?; // number of symbol table entries + self.writer.write_all(&[0; 4])?; // optional header size = 0, characteristics = 0 + + // Write the section header. + self.writer.write_all(b".rsrc\0\0\0")?; + self.writer.write_all(&[0; 8])?; // virtual size = 0 and virtual address = 0 + self.writer.write_all(&self.size_of_raw_data.to_le_bytes())?; + self.writer.write_all(&[60, 0, 0, 0])?; // pointer to raw data + self.writer.write_all(&(self.size_of_raw_data + 60).to_le_bytes())?; // pointer to relocations + self.writer.write_all(&[0; 4])?; // pointer to line numbers + self.writer.write_all(&self.number_of_relocations.to_le_bytes())?; + self.writer.write_all(&[0; 2])?; // number of line numbers + self.writer.write_all(&[0x40, 0, 0x30, 0xc0])?; // characteristics + + // Return the inner writer and dispose of this object. + self.writer.seek(SeekFrom::Start(end_of_file))?; + Ok(self.writer) + } +} + +/// Returns the bytes for a resource directory table. +/// +/// Most of the fields are set to zero, including the timestamp, to aid +/// with making builds reproducible. +/// +/// ```c +/// typedef struct { +/// DWORD Characteristics, +/// DWORD TimeDateStamp, +/// WORD MajorVersion, +/// WORD MinorVersion, +/// WORD NumberOfNamedEntries, +/// WORD NumberOfIdEntries +/// } IMAGE_RESOURCE_DIRECTORY; +/// ``` +pub fn resource_directory_table(number_of_id_entries: u16) -> [u8; 16] { + let mut table = [0; 16]; + table[14..16].copy_from_slice(&number_of_id_entries.to_le_bytes()); + table +} + +/// Returns the bytes for a resource directory entry for an ID. +/// +/// ```c +/// typedef struct { +/// DWORD Name, +/// DWORD OffsetToData +/// } IMAGE_RESOURCE_DIRECTORY_ENTRY; +/// ``` +pub fn resource_directory_id_entry(id: u32, offset: u32, subdirectory: bool) -> [u8; 8] { + let mut entry = [0; 8]; + entry[0..4].copy_from_slice(&id.to_le_bytes()); + let flag: u32 = if subdirectory { 0x80000000 } else { 0 }; + entry[4..8].copy_from_slice(&((offset & 0x7fffffff) | flag).to_le_bytes()); + entry +} + +/// Returns the bytes for a resource data entry. +/// +/// ```c +/// typedef struct { +/// DWORD OffsetToData, +/// DWORD Size, +/// DWORD CodePage, +/// DWORD Reserved +/// } IMAGE_RESOURCE_DATA_ENTRY; +/// ``` +pub fn resource_data_entry(rva: u32, size: u32) -> [u8; 16] { + let mut entry = [0; 16]; + entry[0..4].copy_from_slice(&rva.to_le_bytes()); + entry[4..8].copy_from_slice(&size.to_le_bytes()); + entry +} diff --git a/third_party/rust/embed-manifest/src/embed/error.rs b/third_party/rust/embed-manifest/src/embed/error.rs new file mode 100644 index 0000000000..365d0525ed --- /dev/null +++ b/third_party/rust/embed-manifest/src/embed/error.rs @@ -0,0 +1,57 @@ +//! Error handling for application manifest embedding. + +use std::fmt::{self, Display, Formatter}; +use std::io::{self, ErrorKind}; + +/// The error type which is returned when application manifest embedding fails. +#[derive(Debug)] +pub struct Error { + repr: Repr, +} + +#[derive(Debug)] +enum Repr { + IoError(io::Error), + UnknownTarget, +} + +impl Error { + pub(crate) fn unknown_target() -> Error { + Error { + repr: Repr::UnknownTarget, + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.repr { + Repr::IoError(ref e) => write!(f, "I/O error: {}", e), + Repr::UnknownTarget => f.write_str("unknown target"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self.repr { + Repr::IoError(ref e) => Some(e), + _ => None, + } + } +} + +impl From<io::Error> for Error { + fn from(e: io::Error) -> Self { + Error { repr: Repr::IoError(e) } + } +} + +impl From<Error> for io::Error { + fn from(e: Error) -> Self { + match e.repr { + Repr::IoError(ioe) => ioe, + _ => io::Error::new(ErrorKind::Other, e), + } + } +} diff --git a/third_party/rust/embed-manifest/src/embed/mod.rs b/third_party/rust/embed-manifest/src/embed/mod.rs new file mode 100644 index 0000000000..b054878ac6 --- /dev/null +++ b/third_party/rust/embed-manifest/src/embed/mod.rs @@ -0,0 +1,139 @@ +use std::env; +use std::fs::{self, File}; +use std::io::{self, stdout, BufWriter, Cursor, Write}; +use std::path::{Path, PathBuf}; + +use crate::manifest::ManifestBuilder; + +use self::coff::{resource_data_entry, resource_directory_id_entry, resource_directory_table, CoffWriter, MachineType}; +use self::error::Error; + +mod coff; +pub mod error; + +#[cfg(test)] +mod test; + +/// Embeds the manifest described by `manifest` by converting it to XML, +/// then saving it to a file and passing the correct options to the linker +/// on MSVC targets, or by building a static library and instructing Cargo +/// to link the executable against it on GNU targets. +pub fn embed_manifest(manifest: ManifestBuilder) -> Result<(), Error> { + let out_dir = get_out_dir()?; + let target = get_target()?; + if matches!(target.os, TargetOs::WindowsMsvc) { + let manifest_file = out_dir.join("manifest.xml"); + write!(BufWriter::new(File::create(&manifest_file)?), "{}", manifest)?; + link_manifest_msvc(&manifest_file, &mut stdout().lock()) + } else { + let manifest_data = manifest.to_string(); + link_manifest_gnu(manifest_data.as_bytes(), &out_dir, target.arch, &mut stdout().lock()) + } +} + +/// Directly embeds the manifest in the provided `file` by passing the correct +/// options to the linker on MSVC targets, or by building a static library +/// and instructing Cargo to link the executable against it on GNU targets. +pub fn embed_manifest_file<P: AsRef<Path>>(file: P) -> Result<(), io::Error> { + let out_dir = get_out_dir()?; + let target = get_target()?; + if matches!(target.os, TargetOs::WindowsMsvc) { + Ok(link_manifest_msvc(file.as_ref(), &mut stdout().lock())?) + } else { + let manifest = fs::read(file.as_ref())?; + Ok(link_manifest_gnu(&manifest, &out_dir, target.arch, &mut stdout().lock())?) + } +} + +fn get_out_dir() -> Result<PathBuf, io::Error> { + match env::var_os("OUT_DIR") { + Some(dir) => Ok(PathBuf::from(dir)), + None => env::current_dir(), + } +} + +enum TargetOs { + WindowsGnu, + WindowsMsvc, +} + +struct Target { + arch: MachineType, + os: TargetOs, +} + +fn get_target() -> Result<Target, Error> { + match env::var("TARGET") { + Ok(target) => parse_target(&target), + _ => Err(Error::unknown_target()), + } +} + +fn parse_target(target: &str) -> Result<Target, Error> { + let mut iter = target.splitn(3, '-'); + let arch = match iter.next() { + Some("i686") => MachineType::I386, + Some("aarch64") => MachineType::Aarch64, + Some("x86_64") => MachineType::X86_64, + _ => return Err(Error::unknown_target()), + }; + if iter.next() != Some("pc") { + return Err(Error::unknown_target()); + } + let os = match iter.next() { + Some("windows-gnu") => TargetOs::WindowsGnu, + Some("windows-gnullvm") => TargetOs::WindowsGnu, + Some("windows-msvc") => TargetOs::WindowsMsvc, + _ => return Err(Error::unknown_target()), + }; + Ok(Target { arch, os }) +} + +fn link_manifest_msvc<W: Write>(manifest_path: &Path, out: &mut W) -> Result<(), Error> { + writeln!(out, "cargo:rustc-link-arg-bins=/MANIFEST:EMBED")?; + writeln!( + out, + "cargo:rustc-link-arg-bins=/MANIFESTINPUT:{}", + manifest_path.canonicalize()?.display() + )?; + writeln!(out, "cargo:rustc-link-arg-bins=/MANIFESTUAC:NO")?; + Ok(()) +} + +fn link_manifest_gnu<W: Write>(manifest: &[u8], out_dir: &Path, arch: MachineType, out: &mut W) -> Result<(), Error> { + // Generate a COFF object file containing the manifest in a .rsrc section. + let object_data = create_object_file(manifest, arch)?; + let path = out_dir.join("embed-manifest.o"); + fs::create_dir_all(out_dir)?; + fs::write(&path, object_data)?; + + // Link the manifest with the executable. + writeln!(out, "cargo:rustc-link-arg-bins={}", path.display())?; + Ok(()) +} + +fn create_object_file(manifest: &[u8], arch: MachineType) -> Result<Vec<u8>, io::Error> { + // Start object file with .rsrc section. + let mut obj = CoffWriter::new(Cursor::new(Vec::with_capacity(4096)), arch)?; + + // Create resource directories for type ID 24, name ID 1, language ID 1033. + obj.add_data(&resource_directory_table(1))?; + obj.add_data(&resource_directory_id_entry(24, 24, true))?; + obj.add_data(&resource_directory_table(1))?; + obj.add_data(&resource_directory_id_entry(1, 48, true))?; + obj.add_data(&resource_directory_table(1))?; + obj.add_data(&resource_directory_id_entry(1033, 72, false))?; + + // Add resource data entry with relocated address. + let address = obj.add_data(&resource_data_entry(88, manifest.len() as u32))?; + + // Add the manifest data. + obj.add_data(manifest)?; + obj.align_to(8)?; + + // Write manifest data relocation at the end of the section. + obj.add_relocation(address)?; + + // Finish writing file and return the populated object data. + Ok(obj.finish()?.into_inner()) +} diff --git a/third_party/rust/embed-manifest/src/embed/test.rs b/third_party/rust/embed-manifest/src/embed/test.rs new file mode 100644 index 0000000000..c464c6044c --- /dev/null +++ b/third_party/rust/embed-manifest/src/embed/test.rs @@ -0,0 +1,173 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use object::coff::CoffFile; +use object::pe::{ImageResourceDataEntry, ImageResourceDirectory, ImageResourceDirectoryEntry, IMAGE_RESOURCE_DATA_IS_DIRECTORY}; +use object::{ + pod, Architecture, LittleEndian, Object, ObjectSection, ObjectSymbol, RelocationEncoding, RelocationKind, SectionKind, +}; +use tempfile::{tempdir, TempDir}; + +use crate::embed::coff::MachineType; +use crate::embed::{create_object_file, link_manifest_gnu, link_manifest_msvc, TargetOs}; +use crate::new_manifest; + +#[test] +fn create_obj() { + let res = do_embed_file(MachineType::X86_64, TargetOs::WindowsGnu); + let data = fs::read(&res.object_file()).unwrap(); + let obj = CoffFile::parse(&data[..]).unwrap(); + assert_eq!(obj.architecture(), Architecture::X86_64); + let expected_manifest = fs::read(&sample_manifest_path()).unwrap(); + check_object_file(obj, &expected_manifest); +} + +#[test] +fn link_lib_gnu() { + let res = do_embed_file(MachineType::X86_64, TargetOs::WindowsGnu); + assert!(res.object_file().exists()); + let object_option = format!("cargo:rustc-link-arg-bins={}", res.object_file().display()); + assert_eq!(res.lines(), &[object_option.as_str()]); +} + +#[test] +fn link_file_msvc() { + let res = do_embed_file(MachineType::X86_64, TargetOs::WindowsMsvc); + assert!(!res.object_file().exists()); + let mut input_option = String::from("cargo:rustc-link-arg-bins=/MANIFESTINPUT:"); + input_option.push_str(res.manifest_path.canonicalize().unwrap().to_str().unwrap()); + assert_eq!( + res.lines(), + &[ + "cargo:rustc-link-arg-bins=/MANIFEST:EMBED", + input_option.as_str(), + "cargo:rustc-link-arg-bins=/MANIFESTUAC:NO" + ] + ); +} + +struct EmbedResult { + manifest_path: PathBuf, + out_dir: TempDir, + output: String, +} + +impl EmbedResult { + fn object_file(&self) -> PathBuf { + self.out_dir.path().join("embed-manifest.o") + } + + fn lines(&self) -> Vec<&str> { + self.output.lines().collect() + } +} + +fn sample_manifest_path() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata/sample.exe.manifest") +} + +fn do_embed_file(arch: MachineType, os: TargetOs) -> EmbedResult { + let manifest_path = sample_manifest_path(); + let out_dir = tempdir().unwrap(); + let mut buf: Vec<u8> = Vec::new(); + if matches!(os, TargetOs::WindowsMsvc) { + link_manifest_msvc(&manifest_path, &mut buf).unwrap(); + } else { + link_manifest_gnu(&fs::read(&manifest_path).unwrap(), out_dir.path(), arch, &mut buf).unwrap(); + } + EmbedResult { + manifest_path, + out_dir, + output: String::from_utf8(buf).unwrap(), + } +} + +#[test] +fn object_file_x86() { + let manifest = new_manifest("Test.X86").to_string().into_bytes(); + let file = create_object_file(&manifest, MachineType::I386).unwrap(); + let obj = CoffFile::parse(&file[..]).unwrap(); + assert_eq!(obj.architecture(), Architecture::I386); + check_object_file(obj, &manifest); +} + +#[test] +fn object_file_x86_64() { + let manifest = new_manifest("Test.X86_64").to_string().into_bytes(); + let file = create_object_file(&manifest, MachineType::X86_64).unwrap(); + let obj = CoffFile::parse(&file[..]).unwrap(); + assert_eq!(obj.architecture(), Architecture::X86_64); + check_object_file(obj, &manifest); +} + +#[test] +fn object_file_aarch64() { + let manifest = new_manifest("Test.AARCH64").to_string().into_bytes(); + let file = create_object_file(&manifest, MachineType::Aarch64).unwrap(); + let obj = CoffFile::parse(&file[..]).unwrap(); + assert_eq!(obj.architecture(), Architecture::Aarch64); + check_object_file(obj, &manifest); +} + +fn check_object_file(obj: CoffFile, expected_manifest: &[u8]) { + // There should be one sections `.rsrc`. + assert_eq!( + obj.sections().map(|s| s.name().unwrap().to_string()).collect::<Vec<_>>(), + &[".rsrc"] + ); + + // There should be one section symbol. + assert_eq!( + obj.symbols().map(|s| s.name().unwrap().to_string()).collect::<Vec<_>>(), + &[".rsrc"] + ); + + // The resource section must be a data section. + let rsrc = obj.section_by_name(".rsrc").unwrap(); + assert_eq!(rsrc.address(), 0); + assert_eq!(rsrc.kind(), SectionKind::Data); + + // The data RVA in the resource data entry must be relocatable. + let (addr, reloc) = rsrc.relocations().next().unwrap(); + assert_eq!(reloc.kind(), RelocationKind::ImageOffset); + assert_eq!(reloc.encoding(), RelocationEncoding::Generic); + assert_eq!(addr, 0x48); // size of the directory table, three directories, and no strings + assert_eq!(reloc.addend(), 0); + + // The resource directory contains one manifest resource type subdirectory. + let data = rsrc.data().unwrap(); + let (dir, rest) = pod::from_bytes::<ImageResourceDirectory>(data).unwrap(); + assert_eq!(0, dir.number_of_named_entries.get(LittleEndian)); + assert_eq!(1, dir.number_of_id_entries.get(LittleEndian)); + let (entries, _) = pod::slice_from_bytes::<ImageResourceDirectoryEntry>(rest, 1).unwrap(); + assert_eq!(24, entries[0].name_or_id.get(LittleEndian)); + let offset = entries[0].offset_to_data_or_directory.get(LittleEndian); + assert_eq!(IMAGE_RESOURCE_DATA_IS_DIRECTORY, offset & IMAGE_RESOURCE_DATA_IS_DIRECTORY); + let offset = (offset & !IMAGE_RESOURCE_DATA_IS_DIRECTORY) as usize; + + // The manifest subdirectory contains one image (not DLL) manifest subdirectory. + let (dir, rest) = pod::from_bytes::<ImageResourceDirectory>(&data[offset..]).unwrap(); + assert_eq!(0, dir.number_of_named_entries.get(LittleEndian)); + assert_eq!(1, dir.number_of_id_entries.get(LittleEndian)); + let (entries, _) = pod::slice_from_bytes::<ImageResourceDirectoryEntry>(rest, 1).unwrap(); + assert_eq!(1, entries[0].name_or_id.get(LittleEndian)); + let offset = entries[0].offset_to_data_or_directory.get(LittleEndian); + assert_eq!(IMAGE_RESOURCE_DATA_IS_DIRECTORY, offset & IMAGE_RESOURCE_DATA_IS_DIRECTORY); + let offset = (offset & !IMAGE_RESOURCE_DATA_IS_DIRECTORY) as usize; + + // The image manifest subdirectory contains one US English manifest data entry. + let (dir, rest) = pod::from_bytes::<ImageResourceDirectory>(&data[offset..]).unwrap(); + assert_eq!(0, dir.number_of_named_entries.get(LittleEndian)); + assert_eq!(1, dir.number_of_id_entries.get(LittleEndian)); + let (entries, _) = pod::slice_from_bytes::<ImageResourceDirectoryEntry>(rest, 1).unwrap(); + assert_eq!(0x0409, entries[0].name_or_id.get(LittleEndian)); + let offset = entries[0].offset_to_data_or_directory.get(LittleEndian); + assert_eq!(0, offset & IMAGE_RESOURCE_DATA_IS_DIRECTORY); + let offset = offset as usize; + + // The manifest data matches what was added. + let (entry, resource_data) = pod::from_bytes::<ImageResourceDataEntry>(&data[offset..]).unwrap(); + let end = entry.size.get(LittleEndian) as usize; + let manifest = &resource_data[..end]; + assert_eq!(manifest, expected_manifest); +} |