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
2 changes: 2 additions & 0 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ jobs:
- ""
- '--no-default-features --features="c-allocator"'
- '--no-default-features --features="rust-allocator"'
- '--no-default-features --features="gz" --features="c-allocator"'
runs-on: ubuntu-latest
steps:
- name: Checkout sources
Expand All @@ -177,6 +178,7 @@ jobs:
- name: Run clippy
run: cargo clippy --target ${{matrix.target}} ${{matrix.features}} --workspace --all-targets -- -D warnings
- name: Run clippy (fuzzers)
if: ${{ !contains(matrix.features, 'gz') }}
run: cargo clippy --target ${{matrix.target}} ${{matrix.features}} --manifest-path ./fuzz/Cargo.toml --all-targets -- -D warnings
- name: Run cargo doc, deny warnings
env:
Expand Down
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions libz-rs-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export-symbols = [] # whether the zlib api symbols are publicly exported
custom-prefix = ["export-symbols"] # use the LIBZ_RS_SYS_PREFIX to prefix all exported symbols
testing-prefix = ["export-symbols"] # prefix all symbols with LIBZ_RS_SYS_TEST_ for testing
semver-prefix = ["export-symbols"] # prefix all symbols in a semver-compatible way
gz = ["dep:libc"] # gzip support has to be configured explicitly, to avoid a libc dependency in the core zlib-rs

[dependencies]
zlib-rs = { workspace = true, default-features = false }
libc = { version = "0.2.171", optional = true }
252 changes: 252 additions & 0 deletions libz-rs-sys/src/gz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
use zlib_rs::allocate::*;
pub use zlib_rs::c_api::*;

use core::ffi::{c_char, c_int, c_uint, CStr};
use core::ptr;
use libc::{O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_TRUNC, O_WRONLY, SEEK_CUR, SEEK_END};
use zlib_rs::deflate::Strategy;

/// In the zlib C API, this structure exposes just enough of the internal state
/// of an open gzFile to support the gzgetc() C macro. Since Rust code won't be
/// using that C macro, we define gzFile_s as an empty structure. The first fields
/// in GzState match what would be in the C version of gzFile_s.
pub enum gzFile_s {}

/// File handle for an open gzip file.
pub type gzFile = *mut gzFile_s;

// The internals of a gzip file handle (the thing gzFile actually points to, with the
// public gzFile_s part at the front for ABI compatibility).
#[repr(C)]
struct GzState {
// Public interface:
// These first three fields must match the structure gzFile_s in the C version
// of zlib. In the C library, a macro called gzgetc() reads and writes these
// fields directly.
have: c_uint, // number of bytes available at next
next: *const Bytef, // next byte of uncompressed data
pos: u64, // current offset in uncompressed data stream

// End of public interface:
// All fields after this point are opaque to C code using this library,
// so they can be rearranged without breaking compatibility.

// Fields used for both reading and writing
mode: GzMode,
fd: c_int, // file descriptor
path: *const c_char,
size: usize, // buffer size; can be 0 if not yet allocated
want: usize, // requested buffer size
input: *mut Bytef, // input buffer
output: *mut Bytef, // output buffer
direct: bool, // true in pass-through mode, false if processing gzip data

// Fields used just for reading
// FIXME: add the 'how' field when read support is implemented
start: i64,
eof: bool, // whether we have reached the end of the input file
past: bool, // whether a read past the end has been requested

// Fields used just for writing
level: i8,
strategy: Strategy,
reset: bool, // whether a reset is pending after a Z_FINISH

// Fields used for seek requests
skip: i64, // amount to skip (already rewound if backwards)
seek: bool, // whether a seek request is pending

// Error information
err: c_int, // last error (0 if no error)
msg: *const c_char, // error message from last error (NULL if none)

// FIXME: add the zstream field when read/write support is implemented
}

// Gzip operating modes
// NOTE: These values match what zlib-ng uses.
#[derive(PartialEq, Eq)]
enum GzMode {
GZ_NONE = 0,
GZ_READ = 7247,
GZ_WRITE = 31153,
GZ_APPEND = 1,
}

const GZBUFSIZE: usize = 128 * 1024;

#[cfg(feature = "rust-allocator")]
const ALLOCATOR: &Allocator = &Allocator::RUST;

#[cfg(not(feature = "rust-allocator"))]
#[cfg(feature = "c-allocator")]
const ALLOCATOR: &Allocator = &Allocator::C;

#[cfg(not(feature = "rust-allocator"))]
#[cfg(not(feature = "c-allocator"))]
compile_error!("Either rust-allocator or c-allocator feature is required");

/// Open a gzip file for reading or writing.
///
/// # Safety
///
/// The caller must ensure that path and mode point to valid C strings. If the
/// return value is non-NULL, caller must delete it using only [`gzclose`].
#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzopen))]
pub unsafe extern "C-unwind" fn gzopen(path: *const c_char, mode: *const c_char) -> gzFile {
gzopen_help(path, -1, mode)
}

/// Internal implementation shared by gzopen and gzdopen.
///
/// # Safety
/// The caller must ensure that path and mode are NULL or point to valid C strings.
unsafe fn gzopen_help(path: *const c_char, fd: c_int, mode: *const c_char) -> gzFile {
if path.is_null() || mode.is_null() {
return ptr::null_mut();
}

let Some(state) = ALLOCATOR.allocate_zeroed_raw::<GzState>() else {
return ptr::null_mut();
};
let state = state.cast::<GzState>().as_mut();
state.size = 0;
state.want = GZBUFSIZE;
state.msg = ptr::null();

state.mode = GzMode::GZ_NONE;
state.level = crate::Z_DEFAULT_COMPRESSION as i8;
state.strategy = Strategy::Default;
state.direct = false;

let mut exclusive = false;
let mode = CStr::from_ptr(mode);
for &ch in mode.to_bytes() {
if ch.is_ascii_digit() {
state.level = (ch - b'0') as i8;
} else {
// FIXME implement the 'e' flag on platforms where O_CLOEXEC is supported
match ch {
b'r' => state.mode = GzMode::GZ_READ,
b'w' => state.mode = GzMode::GZ_WRITE,
b'a' => state.mode = GzMode::GZ_APPEND,
b'b' => {} // binary mode is the default
b'x' => exclusive = true,
b'f' => state.strategy = Strategy::Filtered,
b'h' => state.strategy = Strategy::HuffmanOnly,
b'R' => state.strategy = Strategy::Rle,
b'F' => state.strategy = Strategy::Fixed,
b'T' => state.direct = true,
_ => {} // for compatibility with zlib-ng, ignore unexpected characters in the mode
}
}
}

// Must specify read, write, or append
if state.mode == GzMode::GZ_NONE {
free_state(state);
return ptr::null_mut();
}

// Can't force transparent read
if state.mode == GzMode::GZ_READ {
if state.direct {
free_state(state);
return ptr::null_mut();
}
state.direct = true;
}

// Save the path name for error messages
// FIXME: support Windows wide characters for compatibility with zlib-ng
let len = libc::strlen(path) + 1;
let Some(path_copy) = ALLOCATOR.allocate_slice_raw::<c_char>(len) else {
free_state(state);
return ptr::null_mut();
};
let path_copy = path_copy.as_ptr().cast::<c_char>();
libc::strncpy(path_copy, path, len);
state.path = path_copy;

// Open the file unless the caller passed a file descriptor.
if fd > -1 {
state.fd = fd;
} else {
let mut oflag: c_int = 0;
if state.mode == GzMode::GZ_READ {
oflag |= O_RDONLY;
} else {
oflag |= O_WRONLY | O_CREAT;
if exclusive {
oflag |= O_EXCL;
}
if state.mode == GzMode::GZ_WRITE {
oflag |= O_TRUNC;
} else {
oflag |= O_APPEND;
}
}
// FIXME: support _wopen for WIN32
state.fd = libc::open(state.path, oflag, 0o666);
if state.fd == -1 {
free_state(state);
return ptr::null_mut();
}
if state.mode == GzMode::GZ_APPEND {
libc::lseek(state.fd, 0, SEEK_END);
state.mode = GzMode::GZ_WRITE;
}
}

if state.mode == GzMode::GZ_READ {
// Save the current position for rewinding
state.start = libc::lseek(state.fd, 0, SEEK_CUR) as _;
if state.start == -1 {
state.start = 0;
}
}

// FIXME verify the file headers, and initialize the inflate/deflate state

// FIXME change this to core::ptr::from_mut(state).cast::<gzFile_s>() once MSRV >= 1.76
(state as *mut GzState).cast::<gzFile_s>()
}

// Deallocate a GzState structure and all heap-allocated fields inside it.
//
// # Safety
//
// The caller must not use the state after passing it to this function.
unsafe fn free_state(state: &mut GzState) {
if !state.path.is_null() {
ALLOCATOR.deallocate::<c_char>(state.path.cast_mut(), libc::strlen(state.path) + 1);
}
ALLOCATOR.deallocate::<GzState>(state, 1);
}

/// Close an open gzip file and free the internal data structures referenced by the file handle.
///
/// # Returns
///
/// * [`Z_ERRNO`] if closing the file failed
/// * [`Z_OK`] otherwise
///
/// # Safety
///
/// This function may be called at most once for any file handle.
#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzclose))]
pub unsafe extern "C-unwind" fn gzclose(file: gzFile) -> c_int {
let Some(state) = file.cast::<GzState>().as_mut() else {
return Z_STREAM_ERROR;
};

// FIXME: once read/write support is added, clean up internal buffers

let err = libc::close(state.fd);
free_state(state);
if err == 0 {
Z_OK
} else {
Z_ERRNO
}
}
8 changes: 8 additions & 0 deletions libz-rs-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ use zlib_rs::{

pub use zlib_rs::c_api::*;

#[cfg(feature = "gz")]
mod gz;

#[cfg(feature = "gz")]
pub use gz::*;

#[cfg(feature = "custom-prefix")]
macro_rules! prefix {
($name:expr) => {
Expand Down Expand Up @@ -84,6 +90,8 @@ macro_rules! prefix {
};
}

pub(crate) use prefix;

#[cfg(all(feature = "rust-allocator", feature = "c-allocator"))]
const _: () =
compile_error!("Only one of `rust-allocator` and `c-allocator` can be enabled at a time");
Expand Down
2 changes: 1 addition & 1 deletion qemu-cargo-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ linker = "s390x-linux-gnu-gcc"
linker = "i686-linux-gnu-gcc"

[target.wasm32-wasip1]
runner = "/home/runner/.wasmtime/bin/wasmtime"
runner = "/home/runner/.wasmtime/bin/wasmtime --dir /home/runner/work/zlib-rs/zlib-rs/test-libz-rs-sys"
3 changes: 2 additions & 1 deletion test-libz-rs-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ rust-version.workspace = true


[features]
default = ["rust-allocator"]
default = ["rust-allocator", "gz"]
c-allocator = ["libz-rs-sys/c-allocator"]
rust-allocator = ["libz-rs-sys/rust-allocator"]
gz = ["libz-rs-sys/gz"]

[dependencies]
zlib-rs = { workspace = true, default-features = false, features = ["std", "c-allocator", "rust-allocator", "__internal-test"] }
Expand Down
47 changes: 47 additions & 0 deletions test-libz-rs-sys/src/gz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use zlib_rs::c_api::*;

use libz_rs_sys::gzclose;
use libz_rs_sys::gzopen;
use std::ffi::CString;
use std::ptr;

// Generate a file path relative to the project's root
macro_rules! path {
($str:expr) => {
concat!(env!("CARGO_MANIFEST_DIR"), "/", $str)
};
}

unsafe fn test_open(path: &str, mode: &str, should_succeed: bool) {
let cpath = CString::new(path).unwrap();
let cmode = CString::new(mode).unwrap();
let handle = gzopen(cpath.as_ptr(), cmode.as_ptr());
assert_eq!(should_succeed, !handle.is_null(), "{path} {mode}");
if !handle.is_null() {
assert_eq!(gzclose(handle), Z_OK);
}
}

#[test]
fn open_close() {
unsafe {
// Open a valid file for reading
test_open(path!("src/test-data/issue-109.gz"), "r", true);

// "b" for binary mode is optional
test_open(path!("src/test-data/issue-109.gz"), "rb", true);

// Mode must include r, w, or a
test_open(path!("src/test-data/issue-109.gz"), "", false);
test_open(path!("src/test-data/issue-109.gz"), "e", false);

// For zlib-ng compatibility, mode can't specify transparent read
test_open(path!("src/test-data/issue-109.gz"), "Tr", false);

// Read of a nonexistent file should fail
test_open(path!("src/test-data/no-such-file.gz"), "r", false);

// Closing a null file handle should return an error instead of crashing
assert_eq!(gzclose(ptr::null_mut()), Z_STREAM_ERROR);
}
}
3 changes: 3 additions & 0 deletions test-libz-rs-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ mod helpers;
mod inflate;
#[cfg(test)]
mod zlib_ng_cve;
#[cfg(test)]
#[cfg(feature = "gz")]
mod gz;

#[cfg(test)]
#[macro_export]
Expand Down
Loading