diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /third_party/rust/embed-manifest/src | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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 | ||||
-rw-r--r-- | third_party/rust/embed-manifest/src/lib.rs | 134 | ||||
-rw-r--r-- | third_party/rust/embed-manifest/src/manifest/mod.rs | 882 | ||||
-rw-r--r-- | third_party/rust/embed-manifest/src/manifest/test.rs | 117 | ||||
-rw-r--r-- | third_party/rust/embed-manifest/src/manifest/xml.rs | 140 |
8 files changed, 1834 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); +} diff --git a/third_party/rust/embed-manifest/src/lib.rs b/third_party/rust/embed-manifest/src/lib.rs new file mode 100644 index 0000000000..6c60cdf541 --- /dev/null +++ b/third_party/rust/embed-manifest/src/lib.rs @@ -0,0 +1,134 @@ +//! The `embed-manifest` crate provides a straightforward way to embed +//! a Windows manifest in an executable, whatever the build environment +//! and even when cross-compiling, without dependencies on external +//! tools from LLVM or MinGW. +//! +//! This should be called from a [build script][1], as shown below. +//! +//! [1]: https://doc.rust-lang.org/cargo/reference/build-scripts.html +//! +//! On MSVC targets, the manifest file is embedded in the executable by +//! instructing Cargo to pass `/MANIFEST` options to `LINK.EXE`. This +//! requires Cargo from Rust 1.56. +//! +//! On GNU targets, the manifest file is added as a resource in a COFF +//! object file, and Cargo is instructed to link this file into the +//! executable, also using functionality from Rust 1.56. +//! +//! # Usage +//! +//! This crate should be added to the `[build-dependencies]` section in +//! your executable’s `Cargo.toml`: +//! +//! ```toml +//! [build-dependencies] +//! embed-manifest = "1.3.1" +//! ``` +//! +//! In the same directory, create a `build.rs` file to call this crate’s +//! code when building for Windows, and to only run once: +//! +//! ``` +//! use embed_manifest::{embed_manifest, new_manifest}; +//! +//! fn main() { +//! # let tempdir = tempfile::tempdir().unwrap(); +//! # std::env::set_var("OUT_DIR", tempdir.path()); +//! # std::env::set_var("TARGET", "x86_64-pc-windows-gnu"); +//! # std::env::set_var("CARGO_CFG_WINDOWS", ""); +//! if std::env::var_os("CARGO_CFG_WINDOWS").is_some() { +//! embed_manifest(new_manifest("Contoso.Sample")).expect("unable to embed manifest file"); +//! } +//! println!("cargo:rerun-if-changed=build.rs"); +//! } +//! ``` +//! +//! To customise the application manifest, use the methods on it to change things like +//! enabling the segment heap: +//! +//! ``` +//! use embed_manifest::{embed_manifest, new_manifest, manifest::HeapType}; +//! +//! fn main() { +//! # let tempdir = tempfile::tempdir().unwrap(); +//! # std::env::set_var("OUT_DIR", tempdir.path()); +//! # std::env::set_var("TARGET", "x86_64-pc-windows-gnu"); +//! # std::env::set_var("CARGO_CFG_WINDOWS", ""); +//! if std::env::var_os("CARGO_CFG_WINDOWS").is_some() { +//! embed_manifest(new_manifest("Contoso.Sample").heap_type(HeapType::SegmentHeap)) +//! .expect("unable to embed manifest file"); +//! } +//! println!("cargo:rerun-if-changed=build.rs"); +//! } +//! ``` +//! +//! or making it always use legacy single-byte API encoding and only declaring compatibility +//! up to Windows 8.1, without checking whether this is a Windows build: +//! +//! ``` +//! use embed_manifest::{embed_manifest, new_manifest}; +//! use embed_manifest::manifest::{ActiveCodePage::Legacy, SupportedOS::*}; +//! +//! fn main() { +//! # let tempdir = tempfile::tempdir().unwrap(); +//! # std::env::set_var("OUT_DIR", tempdir.path()); +//! # std::env::set_var("TARGET", "x86_64-pc-windows-gnu"); +//! let manifest = new_manifest("Contoso.Sample") +//! .active_code_page(Legacy) +//! .supported_os(Windows7..=Windows81); +//! embed_manifest(manifest).expect("unable to embed manifest file"); +//! println!("cargo:rerun-if-changed=build.rs"); +//! } +//! ``` + +#![allow(clippy::needless_doctest_main)] + +pub use embed::error::Error; +pub use embed::{embed_manifest, embed_manifest_file}; + +use crate::manifest::ManifestBuilder; + +mod embed; +pub mod manifest; + +/// Creates a new [`ManifestBuilder`] with sensible defaults, allowing customisation +/// before the Windows application manifest XML is generated. +/// +/// The initial values used by the manifest are: +/// - Version number from the `CARGO_PKG_VERSION_MAJOR`, `CARGO_PKG_VERSION_MINOR` and +/// `CARGO_PKG_VERSION_PATCH` environment variables. This can then be changed with +/// [`version()`][ManifestBuilder::version]. +/// - A dependency on version 6 of the Common Controls so that message boxes and dialogs +/// will use the latest design, and have the best available support for high DPI displays. +/// This can be removed with +/// [`remove_dependency`][ManifestBuilder::remove_dependency]. +/// - [Compatible with Windows from 7 to 11][ManifestBuilder::supported_os], +/// matching the Rust compiler [tier 1 targets][tier1]. +/// - A “[maximum version tested][ManifestBuilder::max_version_tested]” of Windows 10 +/// version 1903. +/// - An [active code page][ManifestBuilder::active_code_page] of UTF-8, so that +/// single-byte Windows APIs will generally interpret Rust strings correctly, starting +/// from Windows 10 version 1903. +/// - [Version 2 of per-monitor high DPI awareness][manifest::DpiAwareness::PerMonitorV2Only], +/// so that user interface elements can be scaled correctly by the application and +/// Common Controls from Windows 10 version 1703. +/// - [Long path awareness][ManifestBuilder::long_path_aware] from Windows 10 version +/// 1607 [when separately enabled][longpaths]. +/// - [Printer driver isolation][ManifestBuilder::printer_driver_isolation] enabled +/// to improve reliability and security. +/// - An [execution level][ManifestBuilder::requested_execution_level] of “as invoker” +/// so that the UAC elevation dialog will never be displayed, regardless of the name +/// of the program, and [UAC Virtualisation][uac] is disabled in 32-bit programs. +/// +/// [tier1]: https://doc.rust-lang.org/nightly/rustc/platform-support.html +/// [longpaths]: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later +/// [uac]: https://docs.microsoft.com/en-us/windows/security/identity-protection/user-account-control/how-user-account-control-works#virtualization +pub fn new_manifest(name: &str) -> ManifestBuilder { + ManifestBuilder::new(name) +} + +/// Creates a new [`ManifestBuilder`] without any settings, allowing creation of +/// a manifest with only desired content. +pub fn empty_manifest() -> ManifestBuilder { + ManifestBuilder::empty() +} diff --git a/third_party/rust/embed-manifest/src/manifest/mod.rs b/third_party/rust/embed-manifest/src/manifest/mod.rs new file mode 100644 index 0000000000..716a3559f8 --- /dev/null +++ b/third_party/rust/embed-manifest/src/manifest/mod.rs @@ -0,0 +1,882 @@ +//! A builder for Windows application manifest XML files. +//! +//! This module allows the construction of application manifests from code with the +//! [`ManifestBuilder`], for use from a Cargo build script. Once configured, the builder +//! should be passed to [`embed_manifest()`][crate::embed_manifest] to generate the +//! correct instructions for Cargo. For any other use, the builder will output the XML +//! when formatted for [`Display`], or with [`to_string()`][ToString]. For more +//! information about the different elements of an application manifest, see +//! [Application Manifests][1] in the Microsoft Windows App Development documentation. +//! +//! [1]: https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests +//! +//! To generate the manifest XML separately, the XML can be output with `write!` or +//! copied to a string with [`to_string()`][ToString]. To produce the manifest XML with +//! extra whitespace for formatting, format it with the ‘alternate’ flag: +//! +//! ``` +//! # use embed_manifest::new_manifest; +//! let builder = new_manifest("Company.OrgUnit.Program"); +//! assert_eq!(format!("{:#}", builder), r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +//! <assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0"> +//! <assemblyIdentity name="Company.OrgUnit.Program" type="win32" version="1.4.0.0"/> +//! <dependency> +//! <dependentAssembly> +//! <assemblyIdentity language="*" name="Microsoft.Windows.Common-Controls" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" type="win32" version="6.0.0.0"/> +//! </dependentAssembly> +//! </dependency> +//! <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> +//! <application> +//! <maxversiontested Id="10.0.18362.1"/> +//! <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> +//! <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> +//! <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> +//! <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> +//! </application> +//! </compatibility> +//! <asmv3:application> +//! <asmv3:windowsSettings> +//! <activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage> +//! <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2</dpiAwareness> +//! <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware> +//! <printerDriverIsolation xmlns="http://schemas.microsoft.com/SMI/2011/WindowsSettings">true</printerDriverIsolation> +//! </asmv3:windowsSettings> +//! </asmv3:application> +//! <asmv3:trustInfo> +//! <asmv3:security> +//! <asmv3:requestedPrivileges> +//! <asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false"/> +//! </asmv3:requestedPrivileges> +//! </asmv3:security> +//! </asmv3:trustInfo> +//! </assembly>"#.replace("\n", "\r\n")) +//! ``` + +use std::fmt::{Display, Formatter}; +use std::ops::RangeBounds; +use std::{env, fmt}; + +use crate::manifest::xml::XmlFormatter; + +mod xml; + +#[cfg(test)] +mod test; + +/// An opaque container to describe the Windows application manifest for the +/// executable. A new instance with reasonable defaults is created with +/// [`new_manifest()`][crate::new_manifest]. +#[derive(Debug)] +pub struct ManifestBuilder { + identity: Option<AssemblyIdentity>, + dependent_assemblies: Vec<AssemblyIdentity>, + compatibility: ApplicationCompatibility, + windows_settings: WindowsSettings, + requested_execution_level: Option<RequestedExecutionLevel>, +} + +impl ManifestBuilder { + pub(crate) fn new(name: &str) -> Self { + ManifestBuilder { + identity: Some(AssemblyIdentity::application(name)), + dependent_assemblies: vec![AssemblyIdentity::new( + "Microsoft.Windows.Common-Controls", + [6, 0, 0, 0], + 0x6595b64144ccf1df, + )], + compatibility: ApplicationCompatibility { + max_version_tested: Some(MaxVersionTested::Windows10Version1903), + supported_os: vec![ + SupportedOS::Windows7, + SupportedOS::Windows8, + SupportedOS::Windows81, + SupportedOS::Windows10, + ], + }, + windows_settings: WindowsSettings::new(), + requested_execution_level: Some(RequestedExecutionLevel { + level: ExecutionLevel::AsInvoker, + ui_access: false, + }), + } + } + + pub(crate) fn empty() -> Self { + ManifestBuilder { + identity: None, + dependent_assemblies: Vec::new(), + compatibility: ApplicationCompatibility { + max_version_tested: None, + supported_os: Vec::new(), + }, + windows_settings: WindowsSettings::empty(), + requested_execution_level: None, + } + } + + // Set the dot-separated [application name][identity] in the manifest. + // + // [identity]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests#assemblyIdentity + pub fn name(mut self, name: &str) -> Self { + match self.identity { + Some(ref mut identity) => identity.name = name.to_string(), + None => self.identity = Some(AssemblyIdentity::application_version(name, 0, 0, 0, 0)), + } + self + } + + /// Set the four-part application version number in the manifest. + pub fn version(mut self, major: u16, minor: u16, build: u16, revision: u16) -> Self { + match self.identity { + Some(ref mut identity) => identity.version = Version(major, minor, build, revision), + None => { + self.identity = Some(AssemblyIdentity::application_version("", major, minor, build, revision)); + } + } + self + } + + /// Add a dependency on a specific version of a side-by-side assembly + /// to the application manifest. For more information on side-by-side + /// assemblies, see [Using Side-by-side Assemblies][sxs]. + /// + /// [sxs]: https://docs.microsoft.com/en-us/windows/win32/sbscs/using-side-by-side-assemblies + pub fn dependency(mut self, identity: AssemblyIdentity) -> Self { + self.dependent_assemblies.push(identity); + self + } + + /// Remove a dependency on a side-by-side assembly. This can be used to + /// remove the default dependency on Common Controls version 6: + /// + /// ``` + /// # use embed_manifest::new_manifest; + /// new_manifest("Company.OrgUnit.Program") + /// .remove_dependency("Microsoft.Windows.Common-Controls") + /// # ; + /// ``` + pub fn remove_dependency(mut self, name: &str) -> Self { + self.dependent_assemblies.retain(|d| d.name != name); + self + } + + /// Set the “maximum version tested” based on a Windows SDK version. + /// This compatibility setting enables the use of XAML Islands, as described in + /// [Host a standard WinRT XAML control in a C++ desktop (Win32) app][xaml]. + /// + /// [xaml]: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/host-standard-control-with-xaml-islands-cpp + pub fn max_version_tested(mut self, version: MaxVersionTested) -> Self { + self.compatibility.max_version_tested = Some(version); + self + } + + /// Remove the “maximum version tested” from the application compatibility. + pub fn remove_max_version_tested(mut self) -> Self { + self.compatibility.max_version_tested = None; + self + } + + /// Set the range of supported versions of Windows for application compatibility. + /// The default value declares compatibility with every version from + /// [Windows 7][SupportedOS::Windows7] to [Windows 10 and 11][SupportedOS::Windows10]. + pub fn supported_os<R: RangeBounds<SupportedOS>>(mut self, os_range: R) -> Self { + use SupportedOS::*; + + self.compatibility.supported_os.clear(); + for os in [WindowsVista, Windows7, Windows8, Windows81, Windows10] { + if os_range.contains(&os) { + self.compatibility.supported_os.push(os); + } + } + self + } + + /// Set the code page used for single-byte Windows API, starting from Windows 10 + /// version 1903. The default setting of [UTF-8][`ActiveCodePage::Utf8`] makes Rust + /// strings work directly with APIs like `MessageBoxA`. + pub fn active_code_page(mut self, code_page: ActiveCodePage) -> Self { + self.windows_settings.active_code_page = code_page; + self + } + + /// Configures how Windows should display this program on monitors where the + /// graphics need scaling, whether by the application drawing its user + /// interface at different sizes or by the Windows system rendering the graphics + /// to a bitmap then resizing that for display. + pub fn dpi_awareness(mut self, setting: DpiAwareness) -> Self { + self.windows_settings.dpi_awareness = setting; + self + } + + /// Attempts to scale GDI primitives by the per-monitor scaling values, + /// from Windows 10 version 1703. It seems to be best to leave this disabled. + pub fn gdi_scaling(mut self, setting: Setting) -> Self { + self.windows_settings.gdi_scaling = setting.enabled(); + self + } + + /// Select the memory allocator use by the standard heap allocation APIs, + /// including the default Rust allocator. Selecting a different algorithm + /// may make performance and memory use better or worse, so any changes + /// should be carefully tested. + pub fn heap_type(mut self, setting: HeapType) -> Self { + self.windows_settings.heap_type = setting; + self + } + + /// Enable paths longer than 260 characters with some wide-character Win32 APIs, + /// when also enabled in the Windows registry. For more details, see + /// [Maximum Path Length Limitation][1] in the Windows App Development + /// documentation. + /// + /// As of Rust 1.58, the [Rust standard library bypasses this limitation][2] itself + /// by using Unicode paths beginning with `\\?\`. + /// + /// [1]: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation + /// [2]: https://github.com/rust-lang/rust/pull/89174 + pub fn long_path_aware(mut self, setting: Setting) -> Self { + self.windows_settings.long_path_aware = setting.enabled(); + self + } + + /// Enable printer driver isolation for the application, improving security and + /// stability when printing by loading the printer driver in a separate + /// application. This is poorly documented, but is described in a blog post, + /// “[Application-level Printer Driver Isolation][post]”. + /// + /// [post]: https://peteronprogramming.wordpress.com/2018/01/22/application-level-printer-driver-isolation/ + pub fn printer_driver_isolation(mut self, setting: Setting) -> Self { + self.windows_settings.printer_driver_isolation = setting.enabled(); + self + } + + /// Configure whether the application should receive mouse wheel scroll events + /// with a minimum delta of 1, 40 or 120, as described in + /// [Windows precision touchpad devices][touchpad]. + /// + /// [touchpad]: https://docs.microsoft.com/en-us/windows/win32/w8cookbook/windows-precision-touchpad-devices + pub fn scrolling_awareness(mut self, setting: ScrollingAwareness) -> Self { + self.windows_settings.scrolling_awareness = setting; + self + } + + /// Allows the application to disable the filtering that normally + /// removes UWP windows from the results of the `EnumWindows` API. + pub fn window_filtering(mut self, setting: Setting) -> Self { + self.windows_settings.disable_window_filtering = setting.disabled(); + self + } + + /// Selects the authorities to execute the program with, rather than + /// [guessing based on a filename][installer] like `setup.exe`, + /// `update.exe` or `patch.exe`. + /// + /// [installer]: https://docs.microsoft.com/en-us/windows/security/identity-protection/user-account-control/how-user-account-control-works#installer-detection-technology + pub fn requested_execution_level(mut self, level: ExecutionLevel) -> Self { + match self.requested_execution_level { + Some(ref mut requested_execution_level) => requested_execution_level.level = level, + None => self.requested_execution_level = Some(RequestedExecutionLevel { level, ui_access: false }), + } + self + } + + /// Allows the application to access the user interface of applications + /// running with elevated permissions when this program does not, typically + /// for accessibility. The program must additionally be correctly signed + /// and installed in a trusted location like the Program Files directory. + pub fn ui_access(mut self, access: bool) -> Self { + match self.requested_execution_level { + Some(ref mut requested_execution_level) => requested_execution_level.ui_access = access, + None => { + self.requested_execution_level = Some(RequestedExecutionLevel { + level: ExecutionLevel::AsInvoker, + ui_access: access, + }) + } + } + self + } +} + +impl Display for ManifestBuilder { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let mut w = XmlFormatter::new(f); + w.start_document()?; + let mut attrs = vec![("xmlns", "urn:schemas-microsoft-com:asm.v1")]; + if !self.windows_settings.is_empty() || self.requested_execution_level.is_some() { + attrs.push(("xmlns:asmv3", "urn:schemas-microsoft-com:asm.v3")); + } + attrs.push(("manifestVersion", "1.0")); + w.start_element("assembly", &attrs)?; + if let Some(ref identity) = self.identity { + identity.xml_to(&mut w)?; + } + if !self.dependent_assemblies.is_empty() { + w.element("dependency", &[], |w| { + for d in self.dependent_assemblies.as_slice() { + w.element("dependentAssembly", &[], |w| d.xml_to(w))?; + } + Ok(()) + })?; + } + if !self.compatibility.is_empty() { + self.compatibility.xml_to(&mut w)?; + } + if !self.windows_settings.is_empty() { + self.windows_settings.xml_to(&mut w)?; + } + if let Some(ref requested_execution_level) = self.requested_execution_level { + requested_execution_level.xml_to(&mut w)?; + } + w.end_element("assembly") + } +} + +/// Identity of a side-by-side assembly dependency for the application. +#[derive(Debug)] +pub struct AssemblyIdentity { + r#type: AssemblyType, + name: String, + language: Option<String>, + processor_architecture: Option<AssemblyProcessorArchitecture>, + version: Version, + public_key_token: Option<PublicKeyToken>, +} + +impl AssemblyIdentity { + fn application(name: &str) -> AssemblyIdentity { + let major = env::var("CARGO_PKG_VERSION_MAJOR").map_or(0, |s| s.parse().unwrap_or(0)); + let minor = env::var("CARGO_PKG_VERSION_MINOR").map_or(0, |s| s.parse().unwrap_or(0)); + let patch = env::var("CARGO_PKG_VERSION_PATCH").map_or(0, |s| s.parse().unwrap_or(0)); + AssemblyIdentity { + r#type: AssemblyType::Win32, + name: name.to_string(), + language: None, + processor_architecture: None, + version: Version(major, minor, patch, 0), + public_key_token: None, + } + } + + fn application_version(name: &str, major: u16, minor: u16, build: u16, revision: u16) -> AssemblyIdentity { + AssemblyIdentity { + r#type: AssemblyType::Win32, + name: name.to_string(), + language: None, + processor_architecture: None, + version: Version(major, minor, build, revision), + public_key_token: None, + } + } + + /// Creates a new value for a [manifest dependency][ManifestBuilder::dependency], + /// with the `version` as an array of numbers like `[1, 0, 0, 0]` for 1.0.0.0, + /// and the public key token as a 64-bit number like `0x6595b64144ccf1df`. + pub fn new(name: &str, version: [u16; 4], public_key_token: u64) -> AssemblyIdentity { + AssemblyIdentity { + r#type: AssemblyType::Win32, + name: name.to_string(), + language: Some("*".to_string()), + processor_architecture: Some(AssemblyProcessorArchitecture::All), + version: Version(version[0], version[1], version[2], version[3]), + public_key_token: Some(PublicKeyToken(public_key_token)), + } + } + + /// Change the language from `"*"` to the language code in `language`. + pub fn language(mut self, language: &str) -> Self { + self.language = Some(language.to_string()); + self + } + + /// Change the processor architecture from `"*"` to the architecture in `arch`. + pub fn processor_architecture(mut self, arch: AssemblyProcessorArchitecture) -> Self { + self.processor_architecture = Some(arch); + self + } + + fn xml_to(&self, w: &mut XmlFormatter) -> fmt::Result { + let version = self.version.to_string(); + let public_key_token = self.public_key_token.as_ref().map(|token| token.to_string()); + + let mut attrs: Vec<(&str, &str)> = Vec::with_capacity(6); + if let Some(ref language) = self.language { + attrs.push(("language", language)); + } + attrs.push(("name", &self.name)); + if let Some(ref arch) = self.processor_architecture { + attrs.push(("processorArchitecture", arch.as_str())) + } + if let Some(ref token) = public_key_token { + attrs.push(("publicKeyToken", token)); + } + attrs.push(("type", self.r#type.as_str())); + attrs.push(("version", &version)); + w.empty_element("assemblyIdentity", &attrs) + } +} + +#[derive(Debug)] +struct Version(u16, u16, u16, u16); + +impl fmt::Display for Version { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}.{}.{}.{}", self.0, self.1, self.2, self.3) + } +} + +#[derive(Debug)] +struct PublicKeyToken(u64); + +impl fmt::Display for PublicKeyToken { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:016x}", self.0) + } +} + +/// Processor architecture for an assembly identity. +#[derive(Debug)] +#[non_exhaustive] +pub enum AssemblyProcessorArchitecture { + /// Any processor architecture, as `"*"`. + All, + /// 32-bit x86 processors, as `"x86"`. + X86, + /// 64-bit x64 processors, as `"x64"`. + Amd64, + /// 32-bit ARM processors, as `"arm"`. + Arm, + /// 64-bit ARM processors, as `"arm64"`. + Arm64, +} + +impl AssemblyProcessorArchitecture { + pub fn as_str(&self) -> &'static str { + match self { + Self::All => "*", + Self::X86 => "x86", + Self::Amd64 => "amd64", + Self::Arm => "arm", + Self::Arm64 => "arm64", + } + } +} + +impl fmt::Display for AssemblyProcessorArchitecture { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.as_str()) + } +} + +#[derive(Debug)] +#[non_exhaustive] +enum AssemblyType { + Win32, +} + +impl AssemblyType { + fn as_str(&self) -> &'static str { + "win32" + } +} + +#[derive(Debug)] +struct ApplicationCompatibility { + max_version_tested: Option<MaxVersionTested>, + supported_os: Vec<SupportedOS>, +} + +impl ApplicationCompatibility { + fn is_empty(&self) -> bool { + self.supported_os.is_empty() + } + + fn xml_to(&self, w: &mut XmlFormatter) -> fmt::Result { + w.element( + "compatibility", + &[("xmlns", "urn:schemas-microsoft-com:compatibility.v1")], + |w| { + w.element("application", &[], |w| { + if self.supported_os.contains(&SupportedOS::Windows10) { + if let Some(ref version) = self.max_version_tested { + w.empty_element("maxversiontested", &[("Id", version.as_str())])?; + } + } + for os in self.supported_os.iter() { + w.empty_element("supportedOS", &[("Id", os.as_str())])? + } + Ok(()) + }) + }, + ) + } +} + +/// Windows build versions for [`max_version_tested()`][ManifestBuilder::max_version_tested] +/// from the [Windows SDK archive](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/). +#[derive(Debug)] +#[non_exhaustive] +pub enum MaxVersionTested { + /// Windows 10 version 1903, with build version 10.0.18362.0. + Windows10Version1903, + /// Windows 10 version 2004, with build version 10.0.19041.0. + Windows10Version2004, + /// Windows 10 version 2104, with build version 10.0.20348.0. + Windows10Version2104, + /// Windows 11, with build version 10.0.22000.194. + Windows11, + /// Windows 11 version 22H2, with build version 10.0.22621.1. + Windows11Version22H2, +} + +impl MaxVersionTested { + /// Return the Windows version as a string. + pub fn as_str(&self) -> &'static str { + match self { + Self::Windows10Version1903 => "10.0.18362.1", + Self::Windows10Version2004 => "10.0.19041.0", + Self::Windows10Version2104 => "10.0.20348.0", + Self::Windows11 => "10.0.22000.194", + Self::Windows11Version22H2 => "10.0.22621.1", + } + } +} + +impl Display for MaxVersionTested { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.as_str()) + } +} + +/// Operating system versions for Windows compatibility. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +#[non_exhaustive] +pub enum SupportedOS { + /// Windows Vista and Windows Server 2008. + WindowsVista, + /// Windows 7 and Windows Server 2008 R2. + Windows7, + /// Windows 8 and Windows Server 2012. + Windows8, + /// Windows 8.1 and Windows Server 2012 R2. + Windows81, + /// Windows 10 and 11, and Windows Server 2016, 2019 and 2022. + Windows10, +} + +impl SupportedOS { + /// Returns the GUID string for the Windows version. + pub fn as_str(&self) -> &'static str { + match self { + Self::WindowsVista => "{e2011457-1546-43c5-a5fe-008deee3d3f0}", + Self::Windows7 => "{35138b9a-5d96-4fbd-8e2d-a2440225f93a}", + Self::Windows8 => "{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}", + Self::Windows81 => "{1f676c76-80e1-4239-95bb-83d0f6d0da78}", + Self::Windows10 => "{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}", + } + } +} + +impl Display for SupportedOS { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.as_str()) + } +} + +static WS2005: (&str, &str) = ("xmlns", "http://schemas.microsoft.com/SMI/2005/WindowsSettings"); +static WS2011: (&str, &str) = ("xmlns", "http://schemas.microsoft.com/SMI/2011/WindowsSettings"); +static WS2013: (&str, &str) = ("xmlns", "http://schemas.microsoft.com/SMI/2013/WindowsSettings"); +static WS2016: (&str, &str) = ("xmlns", "http://schemas.microsoft.com/SMI/2016/WindowsSettings"); +static WS2017: (&str, &str) = ("xmlns", "http://schemas.microsoft.com/SMI/2017/WindowsSettings"); +static WS2019: (&str, &str) = ("xmlns", "http://schemas.microsoft.com/SMI/2019/WindowsSettings"); +static WS2020: (&str, &str) = ("xmlns", "http://schemas.microsoft.com/SMI/2020/WindowsSettings"); + +#[derive(Debug)] +struct WindowsSettings { + active_code_page: ActiveCodePage, + disable_window_filtering: bool, + dpi_awareness: DpiAwareness, + gdi_scaling: bool, + heap_type: HeapType, + long_path_aware: bool, + printer_driver_isolation: bool, + scrolling_awareness: ScrollingAwareness, +} + +impl WindowsSettings { + fn new() -> Self { + Self { + active_code_page: ActiveCodePage::Utf8, + disable_window_filtering: false, + dpi_awareness: DpiAwareness::PerMonitorV2Only, + gdi_scaling: false, + heap_type: HeapType::LowFragmentationHeap, + long_path_aware: true, + printer_driver_isolation: true, + scrolling_awareness: ScrollingAwareness::UltraHighResolution, + } + } + + fn empty() -> Self { + Self { + active_code_page: ActiveCodePage::System, + disable_window_filtering: false, + dpi_awareness: DpiAwareness::UnawareByDefault, + gdi_scaling: false, + heap_type: HeapType::LowFragmentationHeap, + long_path_aware: false, + printer_driver_isolation: false, + scrolling_awareness: ScrollingAwareness::UltraHighResolution, + } + } + + fn is_empty(&self) -> bool { + matches!( + self, + Self { + active_code_page: ActiveCodePage::System, + disable_window_filtering: false, + dpi_awareness: DpiAwareness::UnawareByDefault, + gdi_scaling: false, + heap_type: HeapType::LowFragmentationHeap, + long_path_aware: false, + printer_driver_isolation: false, + scrolling_awareness: ScrollingAwareness::UltraHighResolution, + } + ) + } + + fn xml_to(&self, w: &mut XmlFormatter) -> fmt::Result { + w.element("asmv3:application", &[], |w| { + w.element("asmv3:windowsSettings", &[], |w| { + self.active_code_page.xml_to(w)?; + if self.disable_window_filtering { + w.element("disableWindowFiltering", &[WS2011], |w| w.text("true"))?; + } + self.dpi_awareness.xml_to(w)?; + if self.gdi_scaling { + w.element("gdiScaling", &[WS2017], |w| w.text("true"))?; + } + if matches!(self.heap_type, HeapType::SegmentHeap) { + w.element("heapType", &[WS2020], |w| w.text("SegmentHeap"))?; + } + if self.long_path_aware { + w.element("longPathAware", &[WS2016], |w| w.text("true"))?; + } + if self.printer_driver_isolation { + w.element("printerDriverIsolation", &[WS2011], |w| w.text("true"))?; + } + self.scrolling_awareness.xml_to(w) + }) + }) + } +} + +/// Configure whether a Windows setting is enabled or disabled, avoiding confusion +/// over which of these options `true` and `false` represent. +#[derive(Debug)] +pub enum Setting { + Disabled = 0, + Enabled = 1, +} + +impl Setting { + /// Returns `true` if the setting should be disabled. + fn disabled(&self) -> bool { + matches!(self, Setting::Disabled) + } + + /// Returns `true` if the setting should be enabled. + fn enabled(&self) -> bool { + matches!(self, Setting::Enabled) + } +} + +/// The code page used by single-byte APIs in the program. +#[derive(Debug)] +#[non_exhaustive] +pub enum ActiveCodePage { + /// Use the code page from the configured system locale, or UTF-8 if “Use Unicode UTF-8 + /// for worldwide language support” is configured. + System, + /// Use UTF-8 from Windows 10 version 1903 and on Windows 11. + Utf8, + /// Use the code page from the configured system locale, even when “Use Unicode UTF-8 + /// for worldwide language support” is configured. + Legacy, + /// Use the code page from the configured system locale on Windows 10, or from this + /// locale on Windows 11. + Locale(String), +} + +impl ActiveCodePage { + pub fn as_str(&self) -> &str { + match self { + Self::System => "", + Self::Utf8 => "UTF-8", + Self::Legacy => "Legacy", + Self::Locale(s) => s, + } + } + + fn xml_to(&self, w: &mut XmlFormatter) -> fmt::Result { + match self { + Self::System => Ok(()), + _ => w.element("activeCodePage", &[WS2019], |w| w.text(self.as_str())), + } + } +} + +impl Display for ActiveCodePage { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.as_str()) + } +} + +/// Options for how Windows will handle drawing on monitors when the graphics +/// need scaling to display at the correct size. +/// +/// See [High DPI Desktop Application Development on Windows][dpi] for more details +/// about the impact of these options. +/// +/// [dpi]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows +#[derive(Debug)] +#[non_exhaustive] +pub enum DpiAwareness { + /// DPI awareness is not configured, so Windows will scale the application unless + /// changed via the `SetProcessDpiAware` or `SetProcessDpiAwareness` API. + UnawareByDefault, + /// DPI awareness is not configured, with Windows 8.1, 10 and 11 not able + /// to change the setting via API. + Unaware, + /// The program uses the system DPI, or the DPI of the monitor they start on if + /// “Fix scaling for apps” is enabled. If the DPI does not match the current + /// monitor then Windows will scale the application. + System, + /// The program uses the system DPI on Windows Vista, 7 and 8, and version 1 of + /// per-monitor DPI awareness on Windows 8.1, 10 and 11. Using version 1 of the + /// API is not recommended. + PerMonitor, + /// The program uses the system DPI on Windows Vista, 7 and 8, version 1 of + /// per-monitor DPI awareness on Windows 8.1 and Windows 10 version 1507 and 1511, + /// and version 2 of per-monitor DPI awareness from Windows 10 version 1607. + PerMonitorV2, + /// The program uses the system DPI on Windows Vista, 7, 8, 8.1 and Windows 10 + /// version 1507 and 1511, and version 2 of per-monitor DPI awareness from + /// Windows 10 version 1607. + PerMonitorV2Only, +} + +impl DpiAwareness { + fn xml_to(&self, w: &mut XmlFormatter) -> fmt::Result { + let settings = match self { + Self::UnawareByDefault => (None, None), + Self::Unaware => (Some("false"), None), + Self::System => (Some("true"), None), + Self::PerMonitor => (Some("true/pm"), None), + Self::PerMonitorV2 => (Some("true/pm"), Some("permonitorv2,permonitor")), + Self::PerMonitorV2Only => (None, Some("permonitorv2")), + }; + if let Some(dpi_aware) = settings.0 { + w.element("dpiAware", &[WS2005], |w| w.text(dpi_aware))?; + } + if let Some(dpi_awareness) = settings.1 { + w.element("dpiAwareness", &[WS2016], |w| w.text(dpi_awareness))?; + } + Ok(()) + } +} + +/// The heap type for the default memory allocator. +#[derive(Debug)] +#[non_exhaustive] +pub enum HeapType { + /// The default heap type since Windows Vista. + LowFragmentationHeap, + /// The modern segment heap, which may reduce total memory allocation in some programs. + /// This is supported since Windows 10 version 2004. See + /// [Improving Memory Usage in Microsoft Edge][edge]. + /// + /// [edge]: https://blogs.windows.com/msedgedev/2020/06/17/improving-memory-usage/ + SegmentHeap, +} + +/// Whether the application supports scroll wheel events with a minimum delta +/// of 1, 40 or 120. +#[derive(Debug)] +#[non_exhaustive] +pub enum ScrollingAwareness { + /// The application can only handle scroll wheel events with the original delta of 120. + LowResolution, + /// The application can handle high precision scroll wheel events with a delta of 40. + HighResolution, + /// The application can handle ultra high precision scroll wheel events with a delta as low as 1. + UltraHighResolution, +} + +impl ScrollingAwareness { + fn xml_to(&self, w: &mut XmlFormatter) -> fmt::Result { + match self { + Self::LowResolution => w.element("ultraHighResolutionScrollingAware", &[WS2013], |w| w.text("false")), + Self::HighResolution => w.element("highResolutionScrollingAware", &[WS2013], |w| w.text("true")), + Self::UltraHighResolution => Ok(()), + } + } +} + +#[derive(Debug)] +struct RequestedExecutionLevel { + level: ExecutionLevel, + ui_access: bool, +} + +impl RequestedExecutionLevel { + fn xml_to(&self, w: &mut XmlFormatter) -> fmt::Result { + w.element("asmv3:trustInfo", &[], |w| { + w.element("asmv3:security", &[], |w| { + w.element("asmv3:requestedPrivileges", &[], |w| { + w.empty_element( + "asmv3:requestedExecutionLevel", + &[ + ("level", self.level.as_str()), + ("uiAccess", if self.ui_access { "true" } else { "false" }), + ], + ) + }) + }) + }) + } +} + +/// The requested execution level for the application when started. +/// +/// The behaviour of each option is described in +/// [Designing UAC Applications for Windows Vista Step 6: Create and Embed an Application Manifest][step6]. +/// +/// [step6]: https://msdn.microsoft.com/en-us/library/bb756929.aspx +#[derive(Debug)] +pub enum ExecutionLevel { + /// The application will always run with the same authorities as the program invoking it. + AsInvoker, + /// The program will run without special authorities for a standard user, but will try to + /// run with administrator authority if the user is an administrator. This is rarely used. + HighestAvailable, + /// The application will run as an administrator, prompting for elevation if necessary. + RequireAdministrator, +} + +impl ExecutionLevel { + pub fn as_str(&self) -> &'static str { + match self { + Self::AsInvoker => "asInvoker", + Self::HighestAvailable => "highestAvailable", + Self::RequireAdministrator => "requireAdministrator", + } + } +} + +impl Display for ExecutionLevel { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.as_str()) + } +} diff --git a/third_party/rust/embed-manifest/src/manifest/test.rs b/third_party/rust/embed-manifest/src/manifest/test.rs new file mode 100644 index 0000000000..9290f7661d --- /dev/null +++ b/third_party/rust/embed-manifest/src/manifest/test.rs @@ -0,0 +1,117 @@ +use super::{ExecutionLevel, SupportedOS}; +use crate::{empty_manifest, new_manifest}; + +fn enc(s: &str) -> String { + let mut buf = String::with_capacity(1024); + buf.push('\u{feff}'); + for l in s.lines() { + buf.push_str(l); + buf.push_str("\r\n"); + } + buf.truncate(buf.len() - 2); + buf +} + +fn encp(s: &str) -> String { + s.replace("\n", "\r\n") +} + +#[test] +fn empty_manifest_canonical() { + let builder = empty_manifest(); + assert_eq!( + format!("{}", builder), + enc(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"></assembly>"#) + ); +} + +#[test] +fn empty_manifest_pretty() { + let builder = empty_manifest(); + assert_eq!( + format!("{:#}", builder), + encp( + r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"/>"# + ) + ); +} + +#[test] +fn only_execution_level() { + let builder = empty_manifest().requested_execution_level(ExecutionLevel::AsInvoker); + assert_eq!( + format!("{:#}", builder), + encp( + r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0"> + <asmv3:trustInfo> + <asmv3:security> + <asmv3:requestedPrivileges> + <asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false"/> + </asmv3:requestedPrivileges> + </asmv3:security> + </asmv3:trustInfo> +</assembly>"# + ) + ); +} + +#[test] +fn only_windows10() { + let builder = empty_manifest().supported_os(SupportedOS::Windows10..); + assert_eq!( + format!("{:#}", builder), + encp( + r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + </application> + </compatibility> +</assembly>"# + ) + ); +} + +#[test] +fn no_comctl32_6() { + let builder = new_manifest("Company.OrgUnit.Program") + .version(1, 0, 0, 2) + .remove_dependency("Microsoft.Windows.Common-Controls"); + assert_eq!( + format!("{:#}", builder), + encp( + r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0"> + <assemblyIdentity name="Company.OrgUnit.Program" type="win32" version="1.0.0.2"/> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <maxversiontested Id="10.0.18362.1"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + </application> + </compatibility> + <asmv3:application> + <asmv3:windowsSettings> + <activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage> + <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2</dpiAwareness> + <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware> + <printerDriverIsolation xmlns="http://schemas.microsoft.com/SMI/2011/WindowsSettings">true</printerDriverIsolation> + </asmv3:windowsSettings> + </asmv3:application> + <asmv3:trustInfo> + <asmv3:security> + <asmv3:requestedPrivileges> + <asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false"/> + </asmv3:requestedPrivileges> + </asmv3:security> + </asmv3:trustInfo> +</assembly>"# + ) + ); +} diff --git a/third_party/rust/embed-manifest/src/manifest/xml.rs b/third_party/rust/embed-manifest/src/manifest/xml.rs new file mode 100644 index 0000000000..a247f42a61 --- /dev/null +++ b/third_party/rust/embed-manifest/src/manifest/xml.rs @@ -0,0 +1,140 @@ +use std::fmt::{self, Display, Formatter, Write}; + +/// Simple but still over-engineered XML generator for a [`Formatter`], for generating +/// Windows Manifest XML. This can easily generate invalid XML. +/// +/// When used correctly, this should generate the same output as MT’s `-canonicalize` +/// option. +pub struct XmlFormatter<'a, 'f> { + f: &'f mut Formatter<'a>, + state: State, + depth: usize, +} + +#[derive(Eq, PartialEq)] +enum State { + Init, + StartTag, + Text, +} + +impl<'a, 'f> XmlFormatter<'a, 'f> { + pub fn new(f: &'f mut Formatter<'a>) -> Self { + Self { + f, + state: State::Init, + depth: 0, + } + } + + fn pretty(&mut self) -> fmt::Result { + if self.f.alternate() { + self.f.write_str("\r\n")?; + for _ in 0..self.depth { + self.f.write_str(" ")?; + } + } + Ok(()) + } + + pub fn start_document(&mut self) -> fmt::Result { + if !self.f.alternate() { + self.f.write_char('\u{FEFF}')?; + } + self.f + .write_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n") + } + + pub fn element<F: FnOnce(&mut Self) -> fmt::Result>(&mut self, name: &str, attrs: &[(&str, &str)], f: F) -> fmt::Result { + self.start_element(name, attrs)?; + f(self)?; + self.end_element(name) + } + + pub fn empty_element(&mut self, name: &str, attrs: &[(&str, &str)]) -> fmt::Result { + self.start_element(name, attrs)?; + self.end_element(name) + } + + pub fn start_element(&mut self, name: &str, attrs: &[(&str, &str)]) -> fmt::Result { + if self.state == State::StartTag { + self.f.write_char('>')?; + } + if self.depth != 0 { + self.pretty()?; + } + write!(self.f, "<{}", name)?; + for (name, value) in attrs { + write!(self.f, " {}=\"{}\"", name, Xml(value))?; + } + self.depth += 1; + self.state = State::StartTag; + Ok(()) + } + + pub fn end_element(&mut self, name: &str) -> fmt::Result { + self.depth -= 1; + match self.state { + State::Init => { + self.pretty()?; + write!(self.f, "</{}>", name) + } + State::Text => { + self.state = State::Init; + write!(self.f, "</{}>", name) + } + State::StartTag => { + self.state = State::Init; + if self.f.alternate() { + self.f.write_str("/>") + } else { + write!(self.f, "></{}>", name) + } + } + } + } + + pub fn text(&mut self, s: &str) -> fmt::Result { + if self.state == State::StartTag { + self.state = State::Text; + self.f.write_char('>')?; + } + Xml(s).fmt(self.f) + } +} + +/// Temporary wrapper for outputting a string with XML attribute encoding. +/// This does not do anything with the control characters which are not +/// valid in XML, encoded or not. +struct Xml<'a>(&'a str); + +impl<'a> Display for Xml<'a> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + // Process the string in blocks separated by special characters, so that the parts that + // don't need encoding can be written all at once, not character by character, and with + // no checks for whether string slices are aligned on character boundaries. + for s in self.0.split_inclusive(&['<', '&', '>', '"', '\r'][..]) { + // Check whether the last character in the substring needs encoding. This will be + // `None` at the end of the input string. + let mut iter = s.chars(); + let ch = match iter.next_back() { + Some('<') => Some("<"), + Some('&') => Some("&"), + Some('>') => Some(">"), + Some('"') => Some("""), + Some('\r') => Some(" "), + _ => None, + }; + // Write the substring except the last character, then the encoded character; + // or the entire substring if it is not terminated by a special character. + match ch { + Some(enc) => { + f.write_str(iter.as_str())?; + f.write_str(enc)?; + } + None => f.write_str(s)?, + } + } + Ok(()) + } +} |