From 9120080eb573377f9ef1c14d2c8d1c295ea376d5 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 27 Oct 2024 16:18:43 -0600 Subject: [PATCH] Support MWCC 4.1+ series compilers (#1) This implements support for MWCC 4.1+ (aka GC 3.0a3 and above). Notable changes: - Hook `CreateFileMappingA` / `MapViewOfFile`: These compilers use these functions rather than `ReadFile` for headers. - Hook `MultiByteToWideChar`: This function's behavior changed in Windows 2000 SP4 / Windows XP, and certain compiler versions rely on the old behavior. This fully reimplements it for CP-932 with the legacy behavior. - Explicitly fail on invalid UTF-8 inputs. Add a warning log when the Shift JIS output is invalid. --- .github/workflows/build.yml | 93 ++++--- Cargo.lock | 224 +++++++++++++--- Cargo.toml | 14 +- src/main.rs | 500 ++++++++++++++++++++++++++++++++---- 4 files changed, 717 insertions(+), 114 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e889d9..541baec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,9 @@ on: pull_request: env: - CARGO_BIN_NAME: sjiswrap + BUILD_PROFILE: release CARGO_TARGET_DIR: target + CARGO_INCREMENTAL: 0 jobs: check: @@ -19,11 +20,14 @@ jobs: RUSTFLAGS: -D warnings steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: components: clippy + toolchain: nightly-2023-09-18 + - name: Cache Rust workspace + uses: Swatinem/rust-cache@v2 - name: Cargo check run: cargo check --features debug --all-targets - name: Cargo clippy @@ -36,53 +40,52 @@ jobs: RUSTFLAGS: -D warnings steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Rust toolchain # We use nightly options in rustfmt.toml - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@master with: components: rustfmt + toolchain: nightly-2023-09-18 - name: Cargo fmt run: cargo fmt --all --check build: name: Build + env: + CARGO_BIN_NAME: sjiswrap strategy: matrix: include: - platform: windows-latest target: i686-pc-windows-msvc name: windows-x86 - build: build fail-fast: false runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@v3 - - name: Install dependencies - if: matrix.packages != '' - run: | - sudo apt-get -y update - sudo apt-get -y install ${{ matrix.packages }} - - name: Install cargo-zigbuild - if: matrix.build == 'zigbuild' - run: pip install ziglang==0.10.1.post1 cargo-zigbuild==0.17.0 + uses: actions/checkout@v4 - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@master with: components: rust-src targets: ${{ matrix.target }} - - name: Cargo build - run: cargo ${{ matrix.build }} --release --features nightly --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} -Z build-std=std,panic_abort - - name: Upload artifacts - uses: actions/upload-artifact@v3 + toolchain: nightly-2023-09-18 + - name: Cache Rust workspace + uses: Swatinem/rust-cache@v2 with: - name: ${{ matrix.name }} + key: ${{ matrix.target }} + - name: Cargo build + run: > + cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} + --bin ${{ env.CARGO_BIN_NAME }} -Z build-std=std,panic_abort --features nightly + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }} path: | - ${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }} - ${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}.exe - ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }} - ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}.exe + ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }} + ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe if-no-files-found: error release: @@ -90,20 +93,52 @@ jobs: if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest needs: [ build ] + permissions: + contents: write steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Check git tag against Cargo version + shell: bash + run: | + set -eou pipefail + tag='${{github.ref}}' + tag="${tag#refs/tags/}" + version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"') + version="v$version" + if [ "$tag" != "$version" ]; then + echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)" + exit 1 + fi - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts - name: Rename artifacts working-directory: artifacts run: | + set -euo pipefail mkdir ../out - for i in */*/release/$CARGO_BIN_NAME*; do - mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/release\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")" + for dir in */; do + for file in "$dir"*; do + base=$(basename "$file") + name="${base%.*}" + ext="${base##*.}" + if [ "$ext" = "$base" ]; then + ext="" + else + ext=".$ext" + fi + arch="${dir%/}" # remove trailing slash + arch="${arch##"$name-"}" # remove bin name + dst="../out/${name}-${arch}${ext}" + mv "$file" "$dst" + done done ls -R ../out - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: out/* + draft: true + generate_release_notes: true diff --git a/Cargo.lock b/Cargo.lock index aa821a0..425cbdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,15 +4,15 @@ version = 3 [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "cfg-if" @@ -43,10 +43,74 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc62ccb14881da5d1862cda3a9648fb4a4897b2aff0b2557b89da44a5e550b7c" [[package]] -name = "num-traits" -version = "0.2.16" +name = "num" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -63,40 +127,132 @@ dependencies = [ ] [[package]] -name = "rustc-hash" -version = "1.1.0" +name = "proc-macro2" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "sjiswrap" -version = "1.1.1" +version = "1.2.0" dependencies = [ "anyhow", "encoding_rs", "memexec", + "num", "rustc-hash", "windows", ] [[package]] -name = "windows" -version = "0.48.0" +name = "syn" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -105,42 +261,48 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 59c5fe5..44b1dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,13 @@ name = "sjiswrap" description = "UTF-8 to Shift JIS wrapper for old compilers." authors = ["Luke Street "] license = "MIT OR Apache-2.0" -version = "1.1.1" +version = "1.2.0" edition = "2021" publish = false repository = "https://github.com/encounter/sjiswrap" readme = "README.md" categories = ["command-line-utilities"] +rust-version = "1.72.0" # Size optimizations [profile.release] @@ -25,13 +26,14 @@ nightly = [ ] [dependencies] -anyhow = "1.0.72" -encoding_rs = "0.8.32" -memexec = { version = "0.2.0", features = ["hook"] } -rustc-hash = "1.1.0" +anyhow = "1.0" +encoding_rs = "0.8" +memexec = { version = "0.2", features = ["hook"] } +num = "0.4" +rustc-hash = "2.0" [dependencies.windows] -version = "0.48.0" +version = "0.58" features = [ "Win32_Foundation", "Win32_Globalization", diff --git a/src/main.rs b/src/main.rs index 0508fc6..3be6143 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,26 +9,38 @@ use std::{ iter::{Cloned, Peekable}, mem::MaybeUninit, path::{Path, PathBuf}, + pin::Pin, process::exit, }; use anyhow::{Context, Result}; -use encoding_rs::SHIFT_JIS; +use encoding_rs::{SHIFT_JIS, UTF_8}; +use num::Zero; use rustc_hash::FxHashMap; use windows::{ core::{PCSTR, PCWSTR}, Win32::{ Foundation::{ CloseHandle, GetLastError, SetLastError, BOOL, ERROR_INSUFFICIENT_BUFFER, - GENERIC_ACCESS_RIGHTS, GENERIC_READ, HANDLE, HMODULE, INVALID_HANDLE_VALUE, + ERROR_NO_UNICODE_TRANSLATION, ERROR_SUCCESS, GENERIC_ACCESS_RIGHTS, GENERIC_READ, + HANDLE, HMODULE, INVALID_HANDLE_VALUE, }, + Globalization::{MultiByteToWideChar, MULTI_BYTE_TO_WIDE_CHAR_FLAGS}, Security::SECURITY_ATTRIBUTES, Storage::FileSystem::{ - CreateFileA, GetFileSize, GetFullPathNameA, ReadFile, SetFilePointer, FILE_BEGIN, - FILE_CREATION_DISPOSITION, FILE_CURRENT, FILE_END, FILE_FLAGS_AND_ATTRIBUTES, - FILE_SHARE_MODE, SET_FILE_POINTER_MOVE_METHOD, + CreateFileA, GetFileSize, GetFullPathNameA, ReadFile, ReadFileEx, SetFilePointer, + FILE_BEGIN, FILE_CREATION_DISPOSITION, FILE_CURRENT, FILE_END, + FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, SET_FILE_POINTER_MOVE_METHOD, + }, + System::{ + Environment::GetCommandLineA, + LibraryLoader::SetDllDirectoryA, + Memory::{ + CreateFileMappingA, MapViewOfFile, UnmapViewOfFile, FILE_MAP, FILE_MAP_ALL_ACCESS, + FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, PAGE_PROTECTION_FLAGS, + }, + IO::{LPOVERLAPPED_COMPLETION_ROUTINE, OVERLAPPED}, }, - System::{Environment::GetCommandLineA, LibraryLoader::SetDllDirectoryA, IO::OVERLAPPED}, }, }; @@ -61,6 +73,15 @@ macro_rules! debug_println { }; } +macro_rules! fail { + ($($arg:tt)*) => { + { + eprintln!($($arg)*); + exit(1); + } + }; +} + fn main() -> Result<()> { let args: Vec = std::env::args_os().collect(); if args.len() < 2 { @@ -99,10 +120,25 @@ fn main() -> Result<()> { hooks.insert("kernel32.dll!GetFileSize".into(), hook_GetFileSize as *const c_void); hooks.insert("kernel32.dll!CloseHandle".into(), hook_CloseHandle as *const c_void); hooks.insert("kernel32.dll!ReadFile".into(), hook_ReadFile as *const c_void); + hooks.insert("kernel32.dll!ReadFileEx".into(), hook_ReadFileEx as *const c_void); hooks.insert("kernel32.dll!SetFilePointer".into(), hook_SetFilePointer as *const c_void); hooks.insert("kernel32.dll!IsDBCSLeadByte".into(), hook_IsDBCSLeadByte as *const c_void); hooks .insert("kernel32.dll!GetModuleFileNameA".into(), hook_GetModuleFileNameA as *const c_void); + hooks + .insert("kernel32.dll!CreateFileMappingA".into(), hook_CreateFileMappingA as *const c_void); + hooks + .insert("kernel32.dll!CreateFileMappingW".into(), hook_CreateFileMappingW as *const c_void); + hooks.insert("kernel32.dll!OpenFileMappingA".into(), hook_OpenFileMappingA as *const c_void); + hooks.insert("kernel32.dll!OpenFileMappingW".into(), hook_OpenFileMappingW as *const c_void); + hooks.insert("kernel32.dll!MapViewOfFile".into(), hook_MapViewOfFile as *const c_void); + hooks.insert("kernel32.dll!MapViewOfFileEx".into(), hook_MapViewOfFileEx as *const c_void); + hooks.insert("kernel32.dll!UnmapViewOfFile".into(), hook_UnmapViewOfFile as *const c_void); + hooks.insert( + "kernel32.dll!MultiByteToWideChar".into(), + hook_MultiByteToWideChar as *const c_void, + ); + hooks.insert("kernel32.dll!GetACP".into(), hook_GetACP as *const c_void); unsafe { memexec::memexec_exe_with_hooks(&buf, &hooks) }.expect("Failed to execute"); Ok(()) } @@ -118,15 +154,21 @@ struct FileHandle { struct GlobalState { exe_path: CString, cmdline: Option, - encoded_files: FxHashMap>, - file_handles: FxHashMap, + encoded_files: FxHashMap>>, + file_handles: FxHashMap<*mut c_void, FileHandle>, + file_mapping_handles: FxHashMap<*mut c_void, HANDLE>, + view_to_mapping: FxHashMap<*mut c_void, HANDLE>, } impl GlobalState { - fn file_by_handle(&mut self, handle: HANDLE) -> Option<(&mut FileHandle, &[u8])> { + fn file_by_handle(&mut self, handle: HANDLE) -> Option<(&mut FileHandle, Pin<&[u8]>)> { self.file_handles .get_mut(&handle.0) - .and_then(|file| self.encoded_files.get(&file.path).map(|data| (file, data.as_slice()))) + .and_then(|file| self.encoded_files.get(&file.path).map(|data| (file, data.as_ref()))) + } + + fn file_by_mapping_handle(&mut self, handle: HANDLE) -> Option<(&mut FileHandle, Pin<&[u8]>)> { + self.file_mapping_handles.get(&handle.0).cloned().and_then(|file| self.file_by_handle(file)) } } @@ -154,7 +196,7 @@ extern "stdcall" fn hook_GetCommandLineA() -> PCSTR { loop { let Some(c) = iter.next() else { if quoted { - panic!("GetCommandLineA(): Unterminated quoted string"); + fail!("sjiswrap: GetCommandLineA(): Unterminated quoted string"); } break; }; @@ -162,8 +204,8 @@ extern "stdcall" fn hook_GetCommandLineA() -> PCSTR { if c == b'"' { let next = iter.next(); if next != Some(b' ') && next.is_some() { - panic!( - "GetCommandLineA(): Expected space after quote, got '{}'", + fail!( + "sjiswrap: GetCommandLineA(): Expected space after quote, got '{}'", char::from(next.unwrap()) ); } @@ -201,7 +243,7 @@ extern "stdcall" fn hook_GetCommandLineA() -> PCSTR { /// `GetCommandLineW` hook. Currently unsupported. extern "stdcall" fn hook_GetCommandLineW() -> PCSTR { - panic!("GetCommandLineW() is not supported"); + fail!("sjiswrap: GetCommandLineW() is not supported"); } /// Read a file into memory and encode it as Shift JIS. @@ -220,32 +262,35 @@ fn encode_file(handle: HANDLE, path: &Path) { return; } - let mut data = vec![0u8; filesize as usize]; + // Include a null terminator for MapViewOfFile + let mut data = vec![0u8; filesize as usize + 1]; let mut bytes_read = 0u32; - if !unsafe { - ReadFile( - handle, - Some(data.as_mut_ptr() as *mut c_void), - data.len() as u32, - Some(&mut bytes_read), - None, - ) + if unsafe { + let slice = &mut data[..filesize as usize]; + ReadFile(handle, Some(slice), Some(&mut bytes_read), None) } - .as_bool() + .is_err() || bytes_read != filesize as u32 { + eprintln!("sjiswrap: Failed to read file {}", path.display()); return; } - let str = unsafe { std::str::from_utf8_unchecked(&data) }; - let (encoded, _, _) = SHIFT_JIS.encode(str); + let str = match std::str::from_utf8(&data) { + Ok(str) => str, + Err(e) => fail!("sjiswrap: File {} is not valid UTF-8: {}", path.display(), e), + }; + let (encoded, _, error) = SHIFT_JIS.encode(str); + if error { + eprintln!("sjiswrap: File {} contains Shift JIS encoding errors", path.display()); + } match encoded { Cow::Borrowed(_) => { // No modifications were made, use the original data - entry.insert(data); + entry.insert(Pin::new(data.into_boxed_slice())); } Cow::Owned(data) => { - entry.insert(data); + entry.insert(Pin::new(data.into_boxed_slice())); } } } @@ -302,21 +347,23 @@ extern "stdcall" fn hook_CreateFileW( _dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES, _hTemplateFile: HANDLE, ) -> HANDLE { - panic!("CreateFileW() is not supported"); + fail!("sjiswrap: CreateFileW() is not supported"); } /// `GetFileSize` hook. If the file was read into memory, return that size instead. extern "stdcall" fn hook_GetFileSize(hFile: HANDLE, lpFileSizeHigh: *mut u32) -> u32 { if !hFile.is_invalid() { let state = unsafe { GLOBAL_STATE.assume_init_mut() }; - if let Some((_handle, data)) = state.file_by_handle(hFile) { - debug_println!("OVERRIDE: GetFileSize({:#X}) = {:#X}", hFile.0, data.len() as u32); - return data.len() as u32; + if let Some((handle, data)) = state.file_by_handle(hFile) { + let _ = handle; + let file_size = data.len() as u32 - 1 /* null terminator */; + debug_println!("OVERRIDE: GetFileSize({}) = {:#X}", handle.path.display(), file_size); + return file_size; } } let ret = unsafe { GetFileSize(hFile, Some(lpFileSizeHigh)) }; - debug_println!("GetFileSize({:#X}, {:?}) = {:#X}", hFile.0, lpFileSizeHigh, ret); + debug_println!("GetFileSize({:p}, {:?}) = {:#X}", hFile.0, lpFileSizeHigh, ret); ret } @@ -326,15 +373,18 @@ extern "stdcall" fn hook_CloseHandle(hObject: HANDLE) -> BOOL { let state = unsafe { GLOBAL_STATE.assume_init_mut() }; if let Some(handle) = state.file_handles.remove(&hObject.0) { let _ = handle; - debug_println!("File handle removed: {:#X} ({})", hObject.0, handle.path.display()); + debug_println!("File handle removed: {:p} ({})", hObject.0, handle.path.display()); // Purposefully leave the file data itself in the cache. // mwcceppc in particular will read the same file multiple times. } + if let Some(_mapping) = state.file_mapping_handles.remove(&hObject.0) { + debug_println!("File mapping handle removed: {:p}", hObject.0); + } } - let ret = unsafe { CloseHandle(hObject) }; - debug_println!("CloseHandle({:#X}) = {:#X}", hObject.0, ret.0); - ret + let ret = unsafe { CloseHandle(hObject) }.is_ok(); + debug_println!("CloseHandle({:p}) = {}", hObject.0, ret); + ret.into() } /// `ReadFile` hook. If the file was read into memory, read from that instead. @@ -348,9 +398,10 @@ extern "stdcall" fn hook_ReadFile( if !hFile.is_invalid() { let state = unsafe { GLOBAL_STATE.assume_init_mut() }; if let Some((handle, data)) = state.file_by_handle(hFile) { + let file_size = data.len() as u64 - 1 /* null terminator */; let count = min( nNumberOfBytesToRead, - u32::try_from(data.len() as u64 - handle.pos).unwrap_or(u32::MAX), + u32::try_from(file_size - handle.pos).unwrap_or(u32::MAX), ); unsafe { std::ptr::copy_nonoverlapping( @@ -364,7 +415,7 @@ extern "stdcall" fn hook_ReadFile( unsafe { *lpNumberOfBytesRead = count }; } debug_println!( - "OVERRIDE: ReadFile({:#X}, {:?}, {:#X}, {:?}) = {:#X}", + "OVERRIDE: ReadFile({:p}, {:?}, {:#X}, {:?}) = {:#X}", hFile.0, lpBuffer, nNumberOfBytesToRead, @@ -378,23 +429,259 @@ extern "stdcall" fn hook_ReadFile( let ret = unsafe { ReadFile( hFile, - Some(lpBuffer), - nNumberOfBytesToRead, + Some(std::slice::from_raw_parts_mut( + lpBuffer as *mut u8, + nNumberOfBytesToRead as usize, + )), Some(lpNumberOfBytesRead), Some(lpOverlapped), ) - }; + } + .is_ok(); debug_println!( - "ReadFile({:#X}, {:?}, {:#X}, {:?}) = {:#X}", + "ReadFile({:p}, {:?}, {:#X}, {:?}) = {}", hFile.0, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, + ret + ); + ret.into() +} + +/// `ReadFileEx` hook. Currently unsupported. +extern "stdcall" fn hook_ReadFileEx( + hFile: HANDLE, + lpBuffer: *mut c_void, + nNumberOfBytesToRead: u32, + lpOverlapped: *mut OVERLAPPED, + lpCompletionRoutine: LPOVERLAPPED_COMPLETION_ROUTINE, +) -> BOOL { + if !hFile.is_invalid() { + let state = unsafe { GLOBAL_STATE.assume_init_mut() }; + if let Some((_handle, _data)) = state.file_by_handle(hFile) { + fail!("sjiswrap: ReadFileEx() is not supported"); + } + } + + // Pass through un-encoded files + let ret = unsafe { + ReadFileEx( + hFile, + if lpBuffer.is_null() { + None + } else { + Some(std::slice::from_raw_parts_mut( + lpBuffer as *mut u8, + nNumberOfBytesToRead as usize, + )) + }, + lpOverlapped, + lpCompletionRoutine, + ) + } + .is_ok(); + debug_println!( + "ReadFileEx({:p}, {:?}, {:#X}, {:?}, {:?}) = {}", + hFile.0, + lpBuffer, + nNumberOfBytesToRead, + lpOverlapped, + lpCompletionRoutine, + ret + ); + ret.into() +} + +/// `CreateFileMappingA` hook. Currently unsupported. +extern "stdcall" fn hook_CreateFileMappingA( + hFile: HANDLE, + lpAttributes: *const SECURITY_ATTRIBUTES, + flProtect: PAGE_PROTECTION_FLAGS, + dwMaximumSizeHigh: u32, + dwMaximumSizeLow: u32, + lpName: PCSTR, +) -> HANDLE { + let ret = unsafe { + CreateFileMappingA( + hFile, + if lpAttributes.is_null() { None } else { Some(lpAttributes) }, + flProtect, + dwMaximumSizeHigh, + dwMaximumSizeLow, + lpName, + ) + } + .unwrap_or(INVALID_HANDLE_VALUE); + + if !hFile.is_invalid() && !ret.is_invalid() { + let state = unsafe { GLOBAL_STATE.assume_init_mut() }; + if let Some((_handle, _data)) = state.file_by_handle(hFile) { + if let Some(existing) = state.file_mapping_handles.insert(ret.0, hFile) { + fail!( + "sjiswrap: CreateFileMappingA({:p}, {:?}, {:#X}, {:#X}, {:#X}, {:?}): Mapping already exists for {:p}", + hFile.0, + lpAttributes, + flProtect.0, + dwMaximumSizeHigh, + dwMaximumSizeLow, + lpName, + existing.0 + ); + } + debug_println!( + "OVERRIDE CreateFileMappingA({:p}, {:?}, {:#X}, {:#X}, {:#X}, {:?}) = {:p}", + hFile.0, + lpAttributes, + flProtect.0, + dwMaximumSizeHigh, + dwMaximumSizeLow, + lpName, + ret.0 + ); + return ret; + } + } + + debug_println!( + "CreateFileMappingA({:p}, {:?}, {:#X}, {:#X}, {:#X}, {:?}) = {:p}", + hFile.0, + lpAttributes, + flProtect.0, + dwMaximumSizeHigh, + dwMaximumSizeLow, + lpName, ret.0 ); ret } +/// `CreateFileMappingW` hook. Currently unsupported. +extern "stdcall" fn hook_CreateFileMappingW( + _hFile: HANDLE, + _lpAttributes: *const SECURITY_ATTRIBUTES, + _flProtect: u32, + _dwMaximumSizeHigh: u32, + _dwMaximumSizeLow: u32, + _lpName: PCWSTR, +) -> HANDLE { + fail!("sjiswrap: CreateFileMappingW() is not supported"); +} + +/// `OpenFileMappingA` hook. Currently unsupported. +extern "stdcall" fn hook_OpenFileMappingA( + _dwDesiredAccess: u32, + _bInheritHandle: BOOL, + _lpName: PCSTR, +) -> HANDLE { + fail!("sjiswrap: OpenFileMappingA() is not supported"); +} + +/// `OpenFileMappingW` hook. Currently unsupported. +extern "stdcall" fn hook_OpenFileMappingW( + _dwDesiredAccess: u32, + _bInheritHandle: BOOL, + _lpName: PCWSTR, +) -> HANDLE { + fail!("sjiswrap: OpenFileMappingW() is not supported"); +} + +/// `MapViewOfFile` hook. If the file was read into memory, return that instead. +extern "stdcall" fn hook_MapViewOfFile( + hFileMappingObject: HANDLE, + dwDesiredAccess: FILE_MAP, + dwFileOffsetHigh: u32, + dwFileOffsetLow: u32, + dwNumberOfBytesToMap: usize, +) -> *mut c_void { + if !hFileMappingObject.is_invalid() { + let state = unsafe { GLOBAL_STATE.assume_init_mut() }; + if let Some((handle, data)) = state.file_by_mapping_handle(hFileMappingObject) { + if dwDesiredAccess.contains(FILE_MAP_WRITE) + || dwDesiredAccess.contains(FILE_MAP_ALL_ACCESS) + { + fail!("sjiswrap: MapViewOfFile(): Write access to encoded file is not supported"); + } + let offset = (dwFileOffsetHigh as u64) << 32 | dwFileOffsetLow as u64; + if offset > 0 { + fail!( + "sjiswrap: MapViewOfFile({}): Offset is not supported ({})", + handle.path.display(), + offset + ); + } + if dwNumberOfBytesToMap != data.len() - 1 + /* null terminator */ + { + fail!( + "sjiswrap: MapViewOfFile({}): Mapping size mismatch ({} != {})", + handle.path.display(), + dwNumberOfBytesToMap, + data.len() - 1 + ); + } + let ptr = data.as_ptr() as *mut c_void; + debug_println!( + "OVERRIDE MapViewOfFile({:p}, {:#X}, {:#X}, {:#X}, {:#X}) = {:p}", + hFileMappingObject.0, + dwDesiredAccess.0, + dwFileOffsetHigh, + dwFileOffsetLow, + dwNumberOfBytesToMap, + ptr + ); + state.view_to_mapping.insert(ptr, hFileMappingObject); + return ptr; + } + } + + let ret = unsafe { + MapViewOfFile( + hFileMappingObject, + dwDesiredAccess, + dwFileOffsetHigh, + dwFileOffsetLow, + dwNumberOfBytesToMap, + ) + }; + debug_println!( + "MapViewOfFile({:p}, {:#X}, {:#X}, {:#X}, {:#X}) = {:p}", + hFileMappingObject.0, + dwDesiredAccess.0, + dwFileOffsetHigh, + dwFileOffsetLow, + dwNumberOfBytesToMap, + ret.Value + ); + ret.Value +} + +/// `MapViewOfFileEx` hook. Currently unsupported. +extern "stdcall" fn hook_MapViewOfFileEx( + _hFileMappingObject: HANDLE, + _dwDesiredAccess: FILE_MAP, + _dwFileOffsetHigh: u32, + _dwFileOffsetLow: u32, + _dwNumberOfBytesToMap: usize, + _lpBaseAddress: *mut c_void, +) -> *mut c_void { + fail!("sjiswrap: MapViewOfFileEx() is not supported"); +} + +/// `UnmapViewOfFile` hook. If the file was read into memory, remove the mapping. +extern "stdcall" fn hook_UnmapViewOfFile(lpBaseAddress: *mut c_void) -> BOOL { + let state = unsafe { GLOBAL_STATE.assume_init_mut() }; + if let Some(_handle) = state.view_to_mapping.remove(&lpBaseAddress) { + debug_println!("OVERRIDE UnmapViewOfFile({:p})", lpBaseAddress); + return true.into(); + } + + let ret = + unsafe { UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: lpBaseAddress }) }.is_ok(); + debug_println!("UnmapViewOfFile({:p}) = {}", lpBaseAddress, ret); + ret.into() +} + /// `SetFilePointer` hook. If the file was read into memory, set the position in that instead. extern "stdcall" fn hook_SetFilePointer( hFile: HANDLE, @@ -408,19 +695,22 @@ extern "stdcall" fn hook_SetFilePointer( let distance_to_move_high = if lpDistanceToMoveHigh.is_null() { 0 } else { unsafe { *lpDistanceToMoveHigh } }; let distance_to_move = lDistanceToMove as i64 | (distance_to_move_high as i64) << 32; - let file_size = data.len() as u64; + let file_size = data.len() as u64 - 1 /* null terminator */; let pos = min( match dwMoveMethod { FILE_BEGIN => distance_to_move as u64, FILE_CURRENT => handle.pos.saturating_add_signed(distance_to_move), FILE_END => file_size.saturating_add_signed(distance_to_move), - _ => panic!("SetFilePointer(): Unsupported move method {:#X}", dwMoveMethod.0), + _ => fail!( + "sjiswrap: SetFilePointer(): Unsupported move method {:#X}", + dwMoveMethod.0 + ), }, file_size, ); handle.pos = pos; debug_println!( - "OVERRIDE SetFilePointer({:#X}, {:#X}, {:?}, {}) = {:#X}", + "OVERRIDE SetFilePointer({:p}, {:#X}, {:?}, {}) = {:#X}", hFile.0, distance_to_move, lpDistanceToMoveHigh, @@ -437,7 +727,7 @@ extern "stdcall" fn hook_SetFilePointer( let ret = unsafe { SetFilePointer(hFile, lDistanceToMove, Some(lpDistanceToMoveHigh), dwMoveMethod) }; debug_println!( - "SetFilePointer({:#X}, {:#X}, {:?}, {}) = {:#X}", + "SetFilePointer({:p}, {:#X}, {:?}, {}) = {:#X}", hFile.0, lDistanceToMove, lpDistanceToMoveHigh, @@ -450,6 +740,120 @@ extern "stdcall" fn hook_SetFilePointer( /// `IsDBCSLeadByte` hook. This normally uses the system codepage, override with Shift JIS behavior. extern "stdcall" fn hook_IsDBCSLeadByte(TestChar: u8) -> BOOL { (TestChar & 0x80 != 0).into() } +fn slice_of(ptr: *const T, len: i32) -> &'static [T] +where T: Copy + Zero { + if ptr.is_null() { + return &[]; + } + if len < 0 { + // Null terminated + let mut len = 0; + while !unsafe { *ptr.offset(len) }.is_zero() { + len += 1; + } + unsafe { std::slice::from_raw_parts(ptr, len as usize) } + } else { + unsafe { std::slice::from_raw_parts(ptr, len as usize) } + } +} + +fn slice_of_mut(ptr: *mut T, len: i32) -> Option<&'static mut [T]> +where T: Copy + Zero { + if ptr.is_null() || len < 0 { + return None; + } + Some(unsafe { std::slice::from_raw_parts_mut(ptr, len as usize) }) +} + +/// `MultiByteToWideChar` hook. This reimplements the conversion for Shift JIS, using the pre-XP +/// behavior of failing on illegal code points. MWCC 3.0 relies on this behavior. +/// See https://learn.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-multibytetowidechar +extern "stdcall" fn hook_MultiByteToWideChar( + CodePage: u32, + dwFlags: MULTI_BYTE_TO_WIDE_CHAR_FLAGS, + lpMultiByteStr: PCSTR, + cbMultiByte: i32, + lpWideCharStr: *mut u16, + cchWideChar: i32, +) -> i32 { + let mb_str = slice_of(lpMultiByteStr.as_ptr(), cbMultiByte); + let mut wide_str = slice_of_mut(lpWideCharStr, cchWideChar); + let decoder = match CodePage { + 0 => UTF_8, + 932 => SHIFT_JIS, + _ => { + // Try to pass through + let ret = + unsafe { MultiByteToWideChar(CodePage, dwFlags, mb_str, wide_str.as_deref_mut()) }; + debug_println!( + "MultiByteToWideChar({}, {:#X}, {:?} ({:X?}), {:?}) = {} ({:?})", + CodePage, + dwFlags.0, + mb_str, + lpMultiByteStr, + wide_str, + ret, + unsafe { GetLastError() } + ); + return ret; + } + }; + let (decoded, _, err) = decoder.decode(mb_str); + let ret = if err { + unsafe { SetLastError(ERROR_NO_UNICODE_TRANSLATION) }; + 0 + } else { + match wide_str.as_deref_mut() { + None => { + unsafe { SetLastError(ERROR_SUCCESS) }; + decoded.encode_utf16().count() as i32 + } + Some(out) => { + let mut out = out.iter_mut(); + let mut written = 0; + for mut c in decoded.encode_utf16() { + if c == 0xFFFD && CodePage == 932 { + // CP-932 replacement character + c = 0x30FB; + } + if let Some(out) = out.next() { + *out = c; + written += 1; + } else { + // Insufficient buffer + written = -1; + break; + } + } + if written < 0 { + unsafe { SetLastError(ERROR_INSUFFICIENT_BUFFER) }; + decoded.encode_utf16().count() as i32 + } else { + unsafe { SetLastError(ERROR_SUCCESS) }; + written + } + } + } + }; + debug_println!( + "OVERRIDE MultiByteToWideChar({}, {:#X}, {:?} ({:X?}), {:?}) = {} ({:?})", + CodePage, + dwFlags.0, + decoded, + mb_str, + wide_str, + ret, + unsafe { GetLastError() } + ); + ret +} + +/// `GetACP` hook. Return the Shift JIS codepage. +extern "stdcall" fn hook_GetACP() -> u32 { + debug_println!("OVERRIDE GetACP() = 932"); + 932 +} + /// `GetModuleFileNameA` hook. Return the absolute path of the executable. extern "stdcall" fn hook_GetModuleFileNameA( hModule: HMODULE, @@ -472,7 +876,7 @@ extern "stdcall" fn hook_GetModuleFileNameA( let slice = unsafe { std::slice::from_raw_parts(lpFilename as *const u8, ret as usize) }; let str = unsafe { std::str::from_utf8_unchecked(slice) }; eprintln!( - "OVERRIDE GetModuleFileNameA({:#X}, {:?}, {:#X}) = {:#X} ({})", + "OVERRIDE GetModuleFileNameA({:p}, {:?}, {:#X}) = {:#X} ({})", hModule.0, lpFilename, nSize, ret, str ); }