From dbb562cb3ef934e5ad82d962c3130cd0df17143e Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:21:58 +0100 Subject: [PATCH] Add support for Windows --- .gitattributes | 1 + .github/workflows/checks.yaml | 8 +- bzip2.rs | 127 +++++++++++++++++++++++--------- bzlib.rs | 34 ++++++++- test-libbzip2-rs-sys/src/lib.rs | 4 +- tests/quick.rs | 74 +++++++++++++++---- tests/recover.rs | 57 +++++++++++--- 7 files changed, 235 insertions(+), 70 deletions(-) diff --git a/.gitattributes b/.gitattributes index 2253303d2..0644cf5b9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,7 @@ # Files that should be left untouched (binary is macro for -text -diff) *.ref binary +tests/*.txt eol=lf # # Exclude files from exporting diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 09c111162..069a3ee0c 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -44,10 +44,10 @@ jobs: os: macos-14 features: "" target: "aarch64-apple-darwin" - # - rust: stable-x86_64-gnu - # os: windows-2022 - # features: "" - # target: "x86_64-pc-windows-gnu" + - rust: stable-x86_64-gnu + os: windows-2022 + features: "" + target: "x86_64-pc-windows-gnu" steps: - name: Checkout sources uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 diff --git a/bzip2.rs b/bzip2.rs index df0312fec..836aeb024 100644 --- a/bzip2.rs +++ b/bzip2.rs @@ -14,12 +14,13 @@ use libbzip2_rs_sys::{ }; use libc::{ - _exit, close, exit, fclose, fdopen, ferror, fflush, fgetc, fileno, fopen, fread, fwrite, open, - perror, remove, rewind, signal, stat, strcat, strcmp, strlen, strncpy, ungetc, utimbuf, write, - FILE, O_CREAT, O_EXCL, O_WRONLY, SIGBUS, SIGHUP, SIGINT, SIGSEGV, SIGTERM, S_IRUSR, S_IWUSR, + _exit, exit, fclose, ferror, fflush, fgetc, fileno, fopen, fread, fwrite, perror, remove, + rewind, signal, stat, strcat, strcmp, strlen, strncpy, ungetc, write, FILE, SIGINT, SIGSEGV, + SIGTERM, }; // FIXME remove this +#[cfg(not(target_os = "windows"))] extern "C" { #[cfg_attr(target_os = "macos", link_name = "__stdinp")] static mut stdin: *mut FILE; @@ -27,6 +28,35 @@ extern "C" { static mut stdout: *mut FILE; } +#[cfg(all(target_os = "windows", target_env = "gnu"))] +extern "C" { + fn __acrt_iob_func(idx: libc::c_uint) -> *mut FILE; +} + +macro_rules! STDIN { + () => { + 'block: { + #[cfg(not(target_os = "windows"))] + break 'block stdin; + + #[cfg(all(target_os = "windows", target_env = "gnu"))] + break 'block __acrt_iob_func(0); + } + }; +} + +macro_rules! STDOUT { + () => { + 'block: { + #[cfg(not(target_os = "windows"))] + break 'block stdout; + + #[cfg(all(target_os = "windows", target_env = "gnu"))] + break 'block __acrt_iob_func(1); + } + }; +} + type Bool = libc::c_uchar; type IntNative = libc::c_int; @@ -123,7 +153,8 @@ unsafe fn compressStream(stream: *mut FILE, zStream: *mut FILE) { let mut bzerr_dummy: i32 = 0; let mut ret: i32; - // TODO set files to binary mode? + set_binary_mode(stream); + set_binary_mode(zStream); if ferror(stream) != 0 { // diverges @@ -197,7 +228,7 @@ unsafe fn compressStream(stream: *mut FILE, zStream: *mut FILE) { ioError() } - if zStream != stdout { + if zStream != STDOUT!() { let fd = fileno(zStream); if fd < 0 { // diverges @@ -289,7 +320,8 @@ unsafe fn uncompressStream(zStream: *mut FILE, stream: *mut FILE) -> bool { let mut state = State::Standard; - // TODO: set the file into binary mode? + set_binary_mode(stream); + set_binary_mode(zStream); if ferror(stream) != 0 || ferror(zStream) != 0 { // diverges @@ -373,7 +405,7 @@ unsafe fn uncompressStream(zStream: *mut FILE, stream: *mut FILE) -> bool { ioError() } - if stream != stdout { + if stream != STDOUT!() { let fd: i32 = fileno(stream); if fd < 0 { // diverges @@ -399,7 +431,7 @@ unsafe fn uncompressStream(zStream: *mut FILE, stream: *mut FILE) -> bool { ioError() } - if stream != stdout { + if stream != STDOUT!() { ret = fclose(stream); outputHandleJustInCase = core::ptr::null_mut(); if ret == libc::EOF { @@ -462,10 +494,10 @@ unsafe fn uncompressStream(zStream: *mut FILE, stream: *mut FILE) -> bool { libbzip2_rs_sys::BZ_MEM_ERROR => outOfMemory(), libbzip2_rs_sys::BZ_UNEXPECTED_EOF => compressedStreamEOF(), libbzip2_rs_sys::BZ_DATA_ERROR_MAGIC => { - if zStream != stdin { + if zStream != STDIN!() { fclose(zStream); } - if stream != stdout { + if stream != STDOUT!() { fclose(stream); } if streamNo == 1 { @@ -591,7 +623,7 @@ unsafe fn testStream(zStream: *mut FILE) -> bool { false } libbzip2_rs_sys::BZ_DATA_ERROR_MAGIC => { - if zStream != stdin { + if zStream != STDIN!() { fclose(zStream); } if streamNo == 1 { @@ -906,19 +938,24 @@ unsafe fn fileExists(name: *mut c_char) -> Bool { exists } unsafe fn fopen_output_safely(name: *mut c_char, mode: *const libc::c_char) -> *mut FILE { - let fh = open( - name, - O_WRONLY | O_CREAT | O_EXCL, - S_IWUSR as libc::c_uint | S_IRUSR as libc::c_uint, - ); - if fh == -1 as libc::c_int { - return std::ptr::null_mut::(); - } - let fp = fdopen(fh, mode); - if fp.is_null() { - close(fh); + #[cfg(unix)] + { + let fh = libc::open( + name, + libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL, + libc::S_IWUSR as libc::c_uint | libc::S_IRUSR as libc::c_uint, + ); + if fh == -1 as libc::c_int { + return std::ptr::null_mut::(); + } + let fp = libc::fdopen(fh, mode); + if fp.is_null() { + libc::close(fh); + } + fp } - fp + #[cfg(not(unix))] + libc::fopen(name, mode) } fn not_a_standard_file(path: &Path) -> bool { @@ -966,7 +1003,7 @@ unsafe fn countHardLinks(name: *mut c_char) -> i32 { (statBuf.st_nlink).wrapping_sub(1) as i32 } #[cfg(not(unix))] -unsafe fn countHardLinks(name: *mut c_char) -> i32 { +unsafe fn countHardLinks(_name: *mut c_char) -> i32 { 0 // FIXME } @@ -982,7 +1019,7 @@ fn count_hardlinks(path: &Path) -> u64 { } #[cfg(not(unix))] -unsafe fn count_hardlinks(path: &Path) -> u64 { +unsafe fn count_hardlinks(_path: &Path) -> u64 { 0 // FIXME } @@ -995,7 +1032,7 @@ unsafe fn saveInputFileMetaInfo(srcName: *mut c_char) { } #[cfg(unix)] unsafe fn applySavedTimeInfoToOutputFile(dstName: *mut c_char) { - let mut uTimBuf: utimbuf = utimbuf { + let mut uTimBuf = libc::utimbuf { actime: 0, modtime: 0, }; @@ -1077,6 +1114,24 @@ unsafe fn hasSuffix(s: *mut c_char, suffix: *const c_char) -> Bool { 0 as Bool } +#[cfg(windows)] +/// Prevent Windows from mangling the read data. +unsafe fn set_binary_mode(file: *mut FILE) { + use std::ffi::c_int; + + extern "C" { + fn _setmode(fd: c_int, mode: c_int) -> c_int; + } + + if _setmode(fileno(file), 0x8000 /* O_BINARY */) == -1 { + ioError(); + } +} + +#[cfg(not(windows))] +/// Prevent Windows from mangling the read data. +unsafe fn set_binary_mode(_file: *mut FILE) {} + unsafe fn compress(name: *mut c_char) { let inStr: *mut FILE; let outStr: *mut FILE; @@ -1208,8 +1263,8 @@ unsafe fn compress(name: *mut c_char) { } match srcMode { SourceMode::I2O => { - inStr = stdin; - outStr = stdout; + inStr = STDIN!(); + outStr = STDOUT!(); if std::io::stdout().is_terminal() { eprintln!( "{}: I won't write compressed data to a terminal.", @@ -1229,7 +1284,7 @@ unsafe fn compress(name: *mut c_char) { inName.as_mut_ptr(), b"rb\0" as *const u8 as *const libc::c_char, ); - outStr = stdout; + outStr = STDOUT!(); if std::io::stdout().is_terminal() { eprintln!( "{}: I won't write compressed data to a terminal.", @@ -1471,8 +1526,8 @@ unsafe fn uncompress(name: Option) { match srcMode { SourceMode::I2O => { - inStr = stdin; - outStr = stdout; + inStr = STDIN!(); + outStr = STDOUT!(); if std::io::stdin().is_terminal() { eprint!( concat!( @@ -1490,7 +1545,7 @@ unsafe fn uncompress(name: Option) { inName.as_mut_ptr(), b"rb\0" as *const u8 as *const libc::c_char, ); - outStr = stdout; + outStr = STDOUT!(); if inStr.is_null() { eprintln!( "{}: Can't open input file {}: {}.", @@ -1679,7 +1734,7 @@ unsafe fn testf(name: Option) { setExit(1); return; } - inStr = stdin; + inStr = STDIN!(); } SourceMode::F2O | SourceMode::F2F => { inStr = fopen( @@ -1814,8 +1869,9 @@ unsafe fn main_0(program_path: &Path) -> IntNative { SIGSEGV, mySIGSEGVorSIGBUScatcher as unsafe extern "C" fn(libc::c_int) as usize, ); + #[cfg(not(target_os = "windows"))] signal( - SIGBUS, + libc::SIGBUS, mySIGSEGVorSIGBUScatcher as unsafe extern "C" fn(libc::c_int) as usize, ); @@ -1991,8 +2047,9 @@ unsafe fn main_0(program_path: &Path) -> IntNative { SIGTERM, mySignalCatcher as unsafe extern "C" fn(IntNative) as usize, ); + #[cfg(not(target_os = "windows"))] signal( - SIGHUP, + libc::SIGHUP, mySignalCatcher as unsafe extern "C" fn(IntNative) as usize, ); } diff --git a/bzlib.rs b/bzlib.rs index 12be97ea8..b3dc88eb5 100644 --- a/bzlib.rs +++ b/bzlib.rs @@ -11,6 +11,7 @@ use crate::libbzip2_rs_sys_version; use crate::BZ_MAX_UNUSED; // FIXME remove this +#[cfg(not(target_os = "windows"))] extern "C" { #[cfg_attr(target_os = "macos", link_name = "__stdinp")] static mut stdin: *mut FILE; @@ -18,6 +19,31 @@ extern "C" { static mut stdout: *mut FILE; } +#[cfg(all(target_os = "windows", target_env = "gnu"))] +extern "C" { + fn __acrt_iob_func(idx: libc::c_uint) -> *mut FILE; +} + +macro_rules! STDIN { + () => {'block: { + #[cfg(not(target_os = "windows"))] + break 'block stdin; + + #[cfg(all(target_os = "windows", target_env = "gnu"))] + break 'block __acrt_iob_func(0); + }}; +} + +macro_rules! STDOUT { + () => {'block: { + #[cfg(not(target_os = "windows"))] + break 'block stdout; + + #[cfg(all(target_os = "windows", target_env = "gnu"))] + break 'block __acrt_iob_func(1); + }}; +} + pub(crate) const BZ_MAX_ALPHA_SIZE: usize = 258; pub(crate) const BZ_MAX_CODE_LEN: usize = 23; @@ -2660,8 +2686,8 @@ unsafe fn bzopen_or_bzdopen(path: Option<&CStr>, open_mode: OpenMode, mode: &CSt let mode2 = mode.as_ptr().cast_mut().cast::(); let default_file = match operation { - Operation::Reading => stdin, - Operation::Writing => stdout, + Operation::Reading => STDIN!(), + Operation::Writing => STDOUT!(), }; let fp = match open_mode { @@ -2696,7 +2722,7 @@ unsafe fn bzopen_or_bzdopen(path: Option<&CStr>, open_mode: OpenMode, mode: &CSt }; if bzfp.is_null() { - if fp != stdin && fp != stdout { + if fp != STDIN!() && fp != STDOUT!() { fclose(fp); } return ptr::null_mut(); @@ -2889,7 +2915,7 @@ pub unsafe extern "C" fn BZ2_bzclose(b: *mut BZFILE) { } } - if fp != stdin && fp != stdout { + if fp != STDIN!() && fp != STDOUT!() { fclose(fp); } } diff --git a/test-libbzip2-rs-sys/src/lib.rs b/test-libbzip2-rs-sys/src/lib.rs index 5df26691e..d53b8b420 100644 --- a/test-libbzip2-rs-sys/src/lib.rs +++ b/test-libbzip2-rs-sys/src/lib.rs @@ -4,7 +4,6 @@ use std::{ ffi::{c_char, c_int, c_void, CStr}, mem::MaybeUninit, - os::fd::{AsRawFd, IntoRawFd}, path::{Path, PathBuf}, }; @@ -1319,7 +1318,10 @@ mod high_level_interface { } #[test] + #[cfg(unix)] fn open_and_close() { + use std::os::fd::{AsRawFd, IntoRawFd}; + let p = std::env::temp_dir().join("open_and_close.bz2"); const RB: *const c_char = b"rb\0".as_ptr().cast::(); diff --git a/tests/quick.rs b/tests/quick.rs index 95bde5260..92f03df91 100644 --- a/tests/quick.rs +++ b/tests/quick.rs @@ -24,8 +24,11 @@ macro_rules! expect_output_failure { ); assert_eq!( - String::from_utf8_lossy(&$output.stderr).replace(bzip2_binary(), "bzip2"), - $expected_stderr, + String::from_utf8_lossy(&$output.stderr) + .replace(bzip2_binary(), "bzip2") + .replace("bzip2.exe", "bzip2") + .replace("\\", "/"), + $expected_stderr.replace("\\", "/"), ); }; } @@ -54,8 +57,11 @@ macro_rules! expect_output_success { ); assert_eq!( - String::from_utf8_lossy(&$output.stderr).replace(bzip2_binary(), "bzip2"), - $expected_stderr, + String::from_utf8_lossy(&$output.stderr) + .replace(bzip2_binary(), "bzip2") + .replace("bzip2.exe", "bzip2") + .replace("\\", "/"), + $expected_stderr.replace("\\", "/"), ); }; } @@ -332,7 +338,11 @@ fn version() { cmd.args(["-V", "--never-processed"]); let output = cmd.output().unwrap(); - assert!(output.status.success(),); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(String::from_utf8_lossy(&output.stdout).contains("This program is free software")); } @@ -341,7 +351,11 @@ fn version() { cmd.args(["--version", "--never-processed"]); let output = cmd.output().unwrap(); - assert!(output.status.success(),); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(String::from_utf8_lossy(&output.stdout).contains("This program is free software")); } } @@ -665,6 +679,7 @@ mod decompress_command { } #[test] + #[cfg(unix)] fn input_file_has_hard_links() { let tmpdir = tempfile::tempdir().unwrap(); let sample1 = tmpdir.path().join("sample1.bz2"); @@ -710,6 +725,7 @@ mod decompress_command { ); } + #[cfg(unix)] #[test] fn input_file_cannot_be_read_f2o() { use std::os::unix::fs::PermissionsExt; @@ -735,6 +751,7 @@ mod decompress_command { } #[test] + #[cfg(unix)] fn output_file_cannot_be_written() { let tmpdir = tempfile::tempdir().unwrap(); let sample1 = tmpdir.path().join("sample1.bz2"); @@ -1127,7 +1144,11 @@ mod compress_command { let output = cmd.output().unwrap(); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); let tmpdir = tempfile::tempdir().unwrap(); @@ -1146,7 +1167,11 @@ mod compress_command { let output = cmd.output().unwrap(); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); let out_hash = crc32fast::hash(&output.stdout); let ref_file = std::fs::read(sample).unwrap(); @@ -1177,10 +1202,10 @@ mod compress_command { String::from_utf8_lossy(&output.stderr) ); - // let tmpdir = tempfile::tempdir().unwrap(); - let tmpdir_path = Path::new("/tmp/foo"); + let tmpdir = tempfile::tempdir().unwrap(); - let tempfile_path = tmpdir_path + let tempfile_path = tmpdir + .path() .with_file_name(sample.file_name().unwrap()) .with_extension("bz2"); @@ -1223,7 +1248,11 @@ mod compress_command { let output = cmd.output().unwrap(); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); let tmpdir = tempfile::tempdir().unwrap(); @@ -1242,7 +1271,11 @@ mod compress_command { let output = cmd.output().unwrap(); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); let out_hash = crc32fast::hash(&output.stdout); let ref_file = std::fs::read(sample).unwrap(); @@ -1267,7 +1300,11 @@ mod compress_command { let output = cmd.output().unwrap(); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert_eq!( String::from_utf8_lossy(&output.stderr).replace(bzip2_binary(), "bzip2"), @@ -1288,7 +1325,11 @@ mod compress_command { let output = cmd.output().unwrap(); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert_eq!( String::from_utf8_lossy(&output.stderr).replace(bzip2_binary(), "bzip2"), @@ -1404,6 +1445,7 @@ mod compress_command { } #[test] + #[cfg(unix)] fn input_file_has_hard_links() { let tmpdir = tempfile::tempdir().unwrap(); let sample1 = tmpdir.path().join("sample1.tar"); @@ -1449,6 +1491,7 @@ mod compress_command { ); } + #[cfg(unix)] #[test] fn input_file_cannot_be_read_f2o() { use std::os::unix::fs::PermissionsExt; @@ -1549,6 +1592,7 @@ mod compress_command { } #[test] + #[cfg(unix)] fn stdout_is_terminal_i2o() { let mut cmd = command(); diff --git a/tests/recover.rs b/tests/recover.rs index 656a05b77..555af9807 100644 --- a/tests/recover.rs +++ b/tests/recover.rs @@ -50,13 +50,18 @@ fn basic_valid_file() { let output = run_bzip2recover(Some(&file_path)); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!( String::from_utf8_lossy(&output.stderr) .replace(&tmp_path_str, "$TEMPDIR") - .replace(bzip2recover_binary(), "bzip2recover"), + .replace(bzip2recover_binary(), "bzip2recover") + .replace("\\", "/"), concat!( "bzip2recover 1.0.6: extracts blocks from damaged .bz2 files.\n", "bzip2recover: searching for block boundaries ...\n", @@ -97,13 +102,18 @@ fn basic_invalid_file() { let output = run_bzip2recover(Some(&file_path)); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!( String::from_utf8_lossy(&output.stderr) .replace(&tmp_path_str, "$TEMPDIR") - .replace(bzip2recover_binary(), "bzip2recover"), + .replace(bzip2recover_binary(), "bzip2recover") + .replace("\\", "/"), concat!( "bzip2recover 1.0.6: extracts blocks from damaged .bz2 files.\n", "bzip2recover: searching for block boundaries ...\n", @@ -127,7 +137,11 @@ fn basic_invalid_file() { fn no_input_file() { let output = run_bzip2recover(None); - assert!(!output.status.success()); + assert!( + !output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!( @@ -144,7 +158,11 @@ fn no_input_file() { fn nonexistent_input_file() { let output = run_bzip2recover(Some(Path::new("does_not_exist.txt"))); - assert!(!output.status.success()); + assert!( + !output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!( @@ -171,7 +189,11 @@ fn random_input_data() { let output = run_bzip2recover(Some(&file_path)); - assert!(!output.status.success()); + assert!( + !output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!( @@ -201,13 +223,18 @@ fn does_not_overwrite_recovered_files() { let output = run_bzip2recover(Some(&file_path)); - assert!(output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!( String::from_utf8_lossy(&output.stderr) .replace(&tmp_path_str, "$TEMPDIR") - .replace(bzip2recover_binary(), "bzip2recover"), + .replace(bzip2recover_binary(), "bzip2recover") + .replace("\\", "/"), concat!( "bzip2recover 1.0.6: extracts blocks from damaged .bz2 files.\n", "bzip2recover: searching for block boundaries ...\n", @@ -225,7 +252,11 @@ fn does_not_overwrite_recovered_files() { let output = run_bzip2recover(Some(&file_path)); - assert!(!output.status.success()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!( @@ -249,7 +280,11 @@ fn very_long_file_name() { let file_path = PathBuf::from("NaN".repeat(1000) + " batman!.txt"); let output = run_bzip2recover(Some(&file_path)); - assert!(!output.status.success()); + assert!( + !output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(output.stdout.is_empty()); assert_eq!(