Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ jobs:
shell: bash
run: cargo fmt --all -- --check

- name: Run cargo check
- name: Run cargo check (gnu)
shell: bash
run: RUSTFLAGS="-D warnings" cargo check --workspace
run: RUSTFLAGS="-D warnings" cargo check --workspace --target 'i686-pc-windows-gnu'

- name: Run cargo test
shell: bash
run: RUSTFLAGS="-D warnings" cargo test --workspace --target 'x86_64-unknown-linux-gnu'

- name: Run cargo build (release)
run: cargo build --verbose --release --package 'zipfixup' --package 'zippatch'
- name: Run cargo build (release gnu)
shell: bash
run: cargo build --verbose --release --package 'zipfixup' --package 'zippatch' --target 'i686-pc-windows-gnu'

- name: Run export checker
run: cargo run --package 'export-check' --target 'x86_64-unknown-linux-gnu'
- name: Run export checker (gnu)
shell: bash
run: cargo run --package 'export-check' --target 'x86_64-unknown-linux-gnu' -- 'target/i686-pc-windows-gnu/release/zipfixup.dll'
208 changes: 109 additions & 99 deletions crates/export-check/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,133 +1,143 @@
use object::read::pe::{ExportTarget, PeFile32};

const EXPECTED_EXPORTS: &[&str] = &["DllMain", "GetTickCount"];
const EXPECTED_FORWARDS: &[&str] = &[
"CloseHandle",
"CompareFileTime",
"CreateDirectoryA",
"CreateEventA",
"CreateFileA",
"CreateMutexA",
"CreateThread",
"DeleteCriticalSection",
"DeleteFileA",
"EnterCriticalSection",
"EnumResourceNamesA",
"ExitProcess",
"FindClose",
"FindFirstFileA",
"FindNextFileA",
"FindResourceExA",
"FormatMessageA",
"FreeLibrary",
"GetCommandLineA",
"GetCurrentDirectoryA",
"GetCurrentThread",
"GetDriveTypeA",
"GetFileSize",
"GetFileTime",
"GetLastError",
"GetLogicalDriveStringsA",
"GetModuleHandleA",
"GetProcAddress",
"GetStartupInfoA",
"GetSystemDefaultLangID",
"GetTempPathA",
"GetThreadPriority",
"GetVersionExA",
"GlobalMemoryStatus",
"InitializeCriticalSection",
"InterlockedDecrement",
"InterlockedIncrement",
"LeaveCriticalSection",
"LoadLibraryA",
"LoadResource",
"LocalFree",
"LockResource",
"lstrlenA",
"MoveFileA",
"OutputDebugStringA",
"OutputDebugStringW",
"QueryPerformanceCounter",
"QueryPerformanceFrequency",
"ReadFile",
"ReleaseMutex",
"ResetEvent",
"SetCurrentDirectoryA",
"SetEvent",
"SetFilePointer",
"SetThreadPriority",
"SizeofResource",
"Sleep",
"WaitForMultipleObjects",
"WaitForSingleObject",
"WriteFile",
];
use object::read::pe::{Export, ExportTarget, PeFile32};
use std::collections::HashSet;
use std::fmt;

const DEF_FILE: &str = include_str!("../../zipfixup/exports.def");

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let contents = std::fs::read("target/i686-pc-windows-gnu/release/zipfixup.dll")?;
let data = contents.as_slice();
let (expected_exports, expected_forwards) = parse_def_file()?;

for path in std::env::args().skip(1) {
println!("=== {} ===", path);

let contents = std::fs::read(path)?;
let data = contents.as_slice();
let pe = PeFile32::parse(data)?;
let table = pe.export_table()?.ok_or("no export table")?;
let exported = table.exports()?;

validate_exports(&exported, &expected_exports, &expected_forwards)?;
}

Ok(())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Forward<'a> {
export: &'a str,
module: &'a str,
forward: &'a str,
}

impl fmt::Display for Forward<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}={}.{}", self.export, self.module, self.forward)
}
}

fn parse_def_file() -> Result<(Vec<&'static str>, Vec<Forward<'static>>), String> {
let mut it = DEF_FILE.lines();
let line = it.next().unwrap();
if line != "LIBRARY ZIPFIXUP" {
return Err(format!("expected `LIBRARY ZIPFIXUP`, found `{line}`"));
}
let line = it.next().unwrap();
if line != "EXPORTS" {
return Err(format!("expected `EXPORTS`, found `{line}`"));
}

let pe = PeFile32::parse(data)?;
let table = pe.export_table()?.ok_or("no export table")?;
let exported = table.exports()?;
let mut exports = Vec::new();
let mut forwards = Vec::new();

for line in it {
let line = line.trim();
if line.is_empty() {
continue;
}

let parse: Vec<&str> = line.split(&['=', '.']).collect();
match &parse[..] {
[export, module, forward] => {
forwards.push(Forward {
export,
module,
forward,
});
}
_ => {
exports.push(line);
}
}
}

Ok((exports, forwards))
}

fn validate_exports(
exported: &[Export<'_>],
expected_exports: &[&'static str],
expected_forwards: &[Forward<'static>],
) -> Result<(), String> {
let mut exports = Vec::new();
let mut forwards = Vec::new();
for export in &exported {
for export in exported {
let name = export
.name
.ok_or_else(|| format!("export has no name: {:?}", export))?;
let name = str::from_utf8(name)?;
.ok_or_else(|| format!("export has no name: {export:?}"))?;
let name =
str::from_utf8(name).map_err(|_e| format!("export name is not unicode: {export:?}"))?;

match export.target {
ExportTarget::ForwardByName(module, forward) => {
if module != b"KERNEL32" {
return Err(format!("expected forward to KERNEL32: {:?}", export).into());
}
if name.as_bytes() != forward {
return Err(format!(
"forward name mismatch `{}` != `{}`",
name,
forward.escape_ascii()
)
.into());
}
forwards.push(name);
let module = str::from_utf8(module)
.map_err(|_e| format!("export module is not unicode: {export:?}"))?;
let forward = str::from_utf8(forward)
.map_err(|_e| format!("export forward is not unicode: {export:?}"))?;

forwards.push(Forward {
export: name,
module,
forward,
});
}
ExportTarget::Address(_addr) => {
exports.push(name);
}
ExportTarget::ForwardByOrdinal(module, _ordinal) => {
if module != b"KERNEL32" {
return Err(format!("expected forward to KERNEL32: {:?}", export).into());
}
return Err(format!("unexpected forward by ordinal: {:?}", export).into());
ExportTarget::ForwardByOrdinal(_module, _ordinal) => {
return Err(format!("unexpected forward by ordinal: {export:?}"));
}
}
}

let mut actual_exports: HashSet<&str> = exports.iter().copied().collect();
let mut actual_forwards: HashSet<Forward<'_>> = forwards.iter().copied().collect();

exports.sort();
forwards.sort();

for name in exports.iter().copied() {
println!("{}", name);
println!("{name}");
}

for name in forwards.iter().copied() {
println!("KERNEL32.{}", name);
for forward in forwards.iter().copied() {
println!("{forward}");
}

for expected in EXPECTED_EXPORTS {
if !exports.contains(expected) {
return Err(format!("missing export `{}`", expected).into());
for expected in expected_exports {
if !actual_exports.remove(expected) {
return Err(format!("missing export `{expected}`"));
}
}

for expected in EXPECTED_FORWARDS {
if !forwards.contains(expected) {
return Err(format!("missing forward `{}`", expected).into());
for expected in expected_forwards {
if !actual_forwards.remove(expected) {
return Err(format!("missing forward `{expected}`"));
}
}

if !actual_forwards.is_empty() {
return Err(format!("unexpected forwards: {actual_forwards:?}"));
}

Ok(())
}
25 changes: 25 additions & 0 deletions crates/zipfixup/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
fn main() {
let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();

let path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let mut path = std::path::PathBuf::from(path);
path.push("exports.def");
let path = format!("{}", path.display());

println!("cargo::warning={path}");
println!("cargo::rerun-if-changed={path}");

match env.as_str() {
"gnu" => {
println!("cargo::warning=GNU");
println!("cargo::rustc-link-arg-cdylib={path}");
}
"msvc" => {
println!("cargo::warning=MSVC");
println!("cargo::rustc-link-arg-cdylib=/DEF:{path}");
}
_ => {
println!("cargo::warning=unknown env `{env}`");
}
}
}
65 changes: 65 additions & 0 deletions crates/zipfixup/exports.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
LIBRARY ZIPFIXUP
EXPORTS
DllMain
GetTickCount

CloseHandle=KERNEL32.CloseHandle
CompareFileTime=KERNEL32.CompareFileTime
CreateDirectoryA=KERNEL32.CreateDirectoryA
CreateEventA=KERNEL32.CreateEventA
CreateFileA=KERNEL32.CreateFileA
CreateMutexA=KERNEL32.CreateMutexA
CreateThread=KERNEL32.CreateThread
DeleteCriticalSection=KERNEL32.DeleteCriticalSection
DeleteFileA=KERNEL32.DeleteFileA
EnterCriticalSection=KERNEL32.EnterCriticalSection
EnumResourceNamesA=KERNEL32.EnumResourceNamesA
ExitProcess=KERNEL32.ExitProcess
FindClose=KERNEL32.FindClose
FindFirstFileA=KERNEL32.FindFirstFileA
FindNextFileA=KERNEL32.FindNextFileA
FindResourceExA=KERNEL32.FindResourceExA
FormatMessageA=KERNEL32.FormatMessageA
FreeLibrary=KERNEL32.FreeLibrary
GetCommandLineA=KERNEL32.GetCommandLineA
GetCurrentDirectoryA=KERNEL32.GetCurrentDirectoryA
GetCurrentThread=KERNEL32.GetCurrentThread
GetDriveTypeA=KERNEL32.GetDriveTypeA
GetFileSize=KERNEL32.GetFileSize
GetFileTime=KERNEL32.GetFileTime
GetLastError=KERNEL32.GetLastError
GetLogicalDriveStringsA=KERNEL32.GetLogicalDriveStringsA
GetModuleHandleA=KERNEL32.GetModuleHandleA
GetProcAddress=KERNEL32.GetProcAddress
GetStartupInfoA=KERNEL32.GetStartupInfoA
GetSystemDefaultLangID=KERNEL32.GetSystemDefaultLangID
GetTempPathA=KERNEL32.GetTempPathA
GetThreadPriority=KERNEL32.GetThreadPriority
GetVersionExA=KERNEL32.GetVersionExA
GlobalMemoryStatus=KERNEL32.GlobalMemoryStatus
InitializeCriticalSection=KERNEL32.InitializeCriticalSection
InterlockedDecrement=KERNEL32.InterlockedDecrement
InterlockedIncrement=KERNEL32.InterlockedIncrement
LeaveCriticalSection=KERNEL32.LeaveCriticalSection
LoadLibraryA=KERNEL32.LoadLibraryA
LoadResource=KERNEL32.LoadResource
LocalFree=KERNEL32.LocalFree
LockResource=KERNEL32.LockResource
lstrlenA=KERNEL32.lstrlenA
MoveFileA=KERNEL32.MoveFileA
OutputDebugStringA=KERNEL32.OutputDebugStringA
OutputDebugStringW=KERNEL32.OutputDebugStringW
QueryPerformanceCounter=KERNEL32.QueryPerformanceCounter
QueryPerformanceFrequency=KERNEL32.QueryPerformanceFrequency
ReadFile=KERNEL32.ReadFile
ReleaseMutex=KERNEL32.ReleaseMutex
ResetEvent=KERNEL32.ResetEvent
SetCurrentDirectoryA=KERNEL32.SetCurrentDirectoryA
SetEvent=KERNEL32.SetEvent
SetFilePointer=KERNEL32.SetFilePointer
SetThreadPriority=KERNEL32.SetThreadPriority
SizeofResource=KERNEL32.SizeofResource
Sleep=KERNEL32.Sleep
WaitForMultipleObjects=KERNEL32.WaitForMultipleObjects
WaitForSingleObject=KERNEL32.WaitForSingleObject
WriteFile=KERNEL32.WriteFile
Loading