Repository: trifectatechfoundation/zlib-rs
Pull request #351: Implement gzputc and gzputs
Author: login: brian-pane
Created: 2025-04-23T19:25:33Z | Merged: 2025-04-24T10:25:18Z



Diff:
diff --git a/libz-rs-sys/src/gz.rs b/libz-rs-sys/src/gz.rs
index 88d7f53a..1c5e07d8 100644
--- a/libz-rs-sys/src/gz.rs
+++ b/libz-rs-sys/src/gz.rs
@@ -11,6 +11,7 @@ use core::ffi::{c_char, c_int, c_uint, c_void, CStr};
 use core::ptr;
 use libc::{O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_TRUNC, O_WRONLY, SEEK_CUR, SEEK_END};
 use std::cmp;
+use std::cmp::Ordering;
 use zlib_rs::deflate::Strategy;
 use zlib_rs::MAX_WBITS;
 
@@ -105,6 +106,32 @@ impl GzState {
 
         Ok((exclusive, cloexec))
     }
+
+    /// Compute the number of bytes of input buffered in `self`.
+    ///
+    /// # Safety
+    ///
+    /// Either
+    /// - `state.input` is null.
+    /// - `state.stream.next_in .. state.stream.next_in + state.stream.avail_in`
+    ///   is contained in `state.input .. state.input + state.in_size`.
+    ///
+    /// It is almost always the case that one of those two conditions is true
+    /// inside this module. The notable exception is in a specific block within
+    /// `gz_write`, where we temporarily set `state.next_in` to point to a
+    /// caller-supplied buffer to do a zero-copy optimization when compressing
+    /// large inputs.
+    unsafe fn input_len(&self) -> usize {
+        if self.input.is_null() {
+            return 0;
+        }
+
+        // Safety: `next_in .. next_in + avail_in` is a subslice, so the preconditions hold.
+        let end = unsafe { self.stream.next_in.add(self.stream.avail_in as usize) };
+
+        // Safety: the caller guarantees that the input slice of `stream` is a subslice of `input`.
+        (unsafe { end.offset_from(self.input) }) as _
+    }
 }
 
 // Gzip operating modes
@@ -1408,13 +1435,8 @@ unsafe fn gz_write(state: &mut GzState, mut buf: *const c_void, mut len: usize)
             if state.stream.avail_in == 0 {
                 state.stream.next_in = state.input;
             }
-            let have = unsafe {
-                state
-                    .stream
-                    .next_in
-                    .add(state.stream.avail_in as usize)
-                    .offset_from(state.input)
-            } as usize;
+            // Safety: `state.stream.next_in` points into the buffer starting at `state.input`.
+            let have = unsafe { state.input_len() };
             let copy = cmp::min(state.in_size.saturating_sub(have), len);
             // Safety: The caller is responsible for ensuring that buf points to at least len readable
             // bytes, and copy is <= len.
@@ -1437,6 +1459,9 @@ unsafe fn gz_write(state: &mut GzState, mut buf: *const c_void, mut len: usize)
         }
 
         // Directly compress user buffer to file.
+        // Note: For this operation, we temporarily break the invariant that
+        // `state.stream.next_in` points to somewhere in the `state.input` buffer.
+        let save_next_in = state.stream.next_in;
         state.stream.next_in = buf.cast::<_>();
         loop {
             let n = cmp::min(len, c_uint::MAX as usize) as c_uint;
@@ -1450,6 +1475,7 @@ unsafe fn gz_write(state: &mut GzState, mut buf: *const c_void, mut len: usize)
                 break;
             }
         }
+        state.stream.next_in = save_next_in;
     }
 
     // Input was all buffered or compressed.
@@ -1738,6 +1764,103 @@ pub unsafe extern "C-unwind" fn gzoffset(file: gzFile) -> z_off_t {
     }
 }
 
+/// Compress and write `c`, converted to an unsigned 8-bit char, into `file`.
+///
+/// # Returns
+///
+///  - The value that was written, on success.
+///  - `-1` on error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+pub unsafe extern "C-unwind" fn gzputc(file: gzFile, c: c_int) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    /* FIXME: Uncomment when seek support is implemented.
+    // Check for seek request.
+    if state.seek {
+        state.seek = false;
+        if gz_zero(state, state.skip) == -1 {
+            return -1;
+        }
+    }
+     */
+
+    // Try writing to input buffer for speed (state.input == null if buffer not initialized).
+    if !state.input.is_null() {
+        if state.stream.avail_in == 0 {
+            state.stream.next_in = state.input;
+        }
+        // Safety: `state.stream.next_in` points into the buffer starting at `state.input`.
+        // (This is an invariant maintained throughout this module, except for a specific
+        // block within `gz_write` that does not call any function that might call `gzputc`.)
+        let have = unsafe { state.input_len() };
+        if have < state.in_size {
+            // Safety: `input` has `in_size` bytes, and `have` < `in_size`.
+            unsafe { *state.input.add(have) = c as u8 };
+            state.stream.avail_in += 1;
+            state.pos += 1;
+            return c & 0xff;
+        }
+    }
+
+    // No room in buffer or not initialized, use gz_write.
+    let buf = [c as u8];
+    // Safety: We have confirmed that `state` is valid, and `buf` contains 1 readable byte of data.
+    match unsafe { gz_write(state, buf.as_ptr().cast::<c_void>(), 1) } {
+        1 => c & 0xff,
+        _ => -1,
+    }
+}
+
+/// Compress and write the given null-terminated string `s` to file, excluding
+/// the terminating null character.
+///
+/// # Returns
+///
+/// - the number of characters written, on success.
+/// - `-1` in case of error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+/// - `s` must point to a null-terminated C string.
+pub unsafe extern "C-unwind" fn gzputs(file: gzFile, s: *const c_char) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    if s.is_null() {
+        return -1;
+    }
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    // Write string.
+    let len = unsafe { libc::strlen(s) };
+    if c_int::try_from(len).is_err() {
+        const MSG: &str = "string length does not fit in int";
+        unsafe { gz_error(state, Some((Z_STREAM_ERROR, MSG))) };
+        return -1;
+    }
+    let put = unsafe { gz_write(state, s.cast::<c_void>(), len) };
+    match put.cmp(&(len as i32)) {
+        Ordering::Less => -1,
+        Ordering::Equal | Ordering::Greater => len as _,
+    }
+}
+
 // Create a deep copy of a C string using `ALLOCATOR`
 //
 // # Safety
diff --git a/test-libz-rs-sys/src/gz.rs b/test-libz-rs-sys/src/gz.rs
index 9355f306..0eabc105 100644
--- a/test-libz-rs-sys/src/gz.rs
+++ b/test-libz-rs-sys/src/gz.rs
@@ -2,7 +2,7 @@ use zlib_rs::c_api::*;
 
 use libz_rs_sys::{
     gzFile_s, gzbuffer, gzclearerr, gzclose, gzclose_r, gzclose_w, gzdirect, gzdopen, gzerror,
-    gzflush, gzoffset, gzopen, gzread, gztell, gzwrite,
+    gzflush, gzoffset, gzopen, gzputc, gzputs, gzread, gztell, gzwrite,
 };
 
 use std::ffi::{c_char, c_int, c_uint, c_void, CString};
@@ -878,6 +878,150 @@ fn gzoffset_gztell_error() {
     }
 }
 
+#[test]
+fn gzputc_basic() {
+    // Create a temporary directory that will be automatically removed when
+    // temp_dir goes out of scope.
+    let temp_dir_path = temp_base();
+    let temp_dir = tempfile::TempDir::new_in(temp_dir_path).unwrap();
+    let temp_path = temp_dir.path();
+    let file_name = path(temp_path, "output");
+
+    // Open a new gzip file for writing. Use direct (uncompressed) mode to make validation easier.
+    let file = unsafe {
+        gzopen(
+            CString::new(file_name.as_str()).unwrap().as_ptr(),
+            CString::new("wT").unwrap().as_ptr(),
+        )
+    };
+    assert!(!file.is_null());
+    // Set a small buffer size to exercise more internal code paths.
+    assert_eq!(unsafe { gzbuffer(file, 8) }, 0);
+
+    // Write to the file one byte at a time, using gzputc.
+    const CONTENT: &[u8] = b"sample text to test gzputc implementation";
+    for c in CONTENT {
+        assert_eq!(unsafe { gzputc(file, *c as _) }, *c as _);
+    }
+
+    // Close the file to flush any buffered writes.
+    assert_eq!(unsafe { gzclose(file) }, Z_OK);
+
+    // Validate that the file contains the expected bytes.
+    let mut mode = 0;
+    #[cfg(target_os = "windows")]
+    {
+        mode |= libc::O_BINARY;
+    }
+    mode |= libc::O_RDONLY;
+    let fd = unsafe { libc::open(CString::new(file_name.as_str()).unwrap().as_ptr(), mode) };
+    assert_ne!(fd, -1);
+    // Try to read more than the expected amount of data, to ensure we get everything.
+    let mut buf = [0u8; CONTENT.len() + 1];
+    let bytes_read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut c_void, buf.len() as _) };
+    assert_ne!(bytes_read, -1);
+    assert_eq!(&buf[..bytes_read as usize], CONTENT);
+    assert_eq!(unsafe { libc::close(fd) }, 0);
+}
+
+#[test]
+fn gzputc_error() {
+    // gzputc on a null file handle should return -1.
+    assert_eq!(unsafe { gzputc(ptr::null_mut(), 1) }, -1);
+
+    // gzputc on a read-only file handle should return -1.
+    let file = unsafe { gzdopen(-2, CString::new("r").unwrap().as_ptr()) };
+    assert!(!file.is_null());
+    assert_eq!(unsafe { gzputc(ptr::null_mut(), 1) }, -1);
+    assert_eq!(unsafe { gzclose(file) }, Z_ERRNO);
+
+    // Open an invalid file descriptor as a gzip write stream, with a small buffer,
+    // and use gzputc to write enough bytes to overflow the buffer and cause file I/O.
+    // The last gzputc call should return -1.
+    let file = unsafe { gzdopen(-2, CString::new("wT").unwrap().as_ptr()) };
+    const BUF_SIZE: usize = 10;
+    assert_eq!(unsafe { gzbuffer(file, BUF_SIZE as _) }, 0);
+    // In write mode, the internal input buffer is 2x the size specified via gzbuffer.
+    for _ in 0..BUF_SIZE * 2 {
+        assert_eq!(unsafe { gzputc(file, 1) }, 1);
+    }
+    assert_eq!(unsafe { gzputc(file, 1) }, -1);
+    assert_eq!(unsafe { gzclose(file) }, Z_ERRNO);
+}
+
+#[test]
+fn gzputs_basic() {
+    // Create a temporary directory that will be automatically removed when
+    // temp_dir goes out of scope.
+    let temp_dir_path = temp_base();
+    let temp_dir = tempfile::TempDir::new_in(temp_dir_path).unwrap();
+    let temp_path = temp_dir.path();
+    let file_name = path(temp_path, "output");
+
+    // Open a new gzip file for writing. Use direct (uncompressed) mode to make validation easier.
+    let file = unsafe {
+        gzopen(
+            CString::new(file_name.as_str()).unwrap().as_ptr(),
+            CString::new("wT").unwrap().as_ptr(),
+        )
+    };
+    assert!(!file.is_null());
+    // Set a small buffer size to exercise more internal code paths.
+    assert_eq!(unsafe { gzbuffer(file, 8) }, 0);
+
+    // gzputs of a null string should return -1 rather than crashing.
+    assert_eq!(unsafe { gzputs(file, ptr::null()) }, -1);
+
+    // Write some data to the file using gzputs.
+    const CONTENT: [&str; 3] = ["zlib ", "", "string larger than the buffer size"];
+    for s in CONTENT {
+        assert_eq!(
+            unsafe { gzputs(file, CString::new(s).unwrap().as_ptr()) },
+            s.len() as _
+        );
+    }
+
+    // Close the file to flush any buffered writes.
+    assert_eq!(unsafe { gzclose(file) }, Z_OK);
+
+    // Validate that the file contains the expected bytes.
+    const EXPECTED: &str = "zlib string larger than the buffer size";
+    let mut mode = 0;
+    #[cfg(target_os = "windows")]
+    {
+        mode |= libc::O_BINARY;
+    }
+    mode |= libc::O_RDONLY;
+    let fd = unsafe { libc::open(CString::new(file_name.as_str()).unwrap().as_ptr(), mode) };
+    assert_ne!(fd, -1);
+    // Try to read more than the expected amount of data, to ensure we get everything.
+    let mut buf = [0u8; EXPECTED.len() + 1];
+    let bytes_read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut c_void, buf.len() as _) };
+    assert_ne!(bytes_read, -1);
+    assert_eq!(&buf[..bytes_read as usize], EXPECTED.as_bytes());
+    assert_eq!(unsafe { libc::close(fd) }, 0);
+}
+
+#[test]
+fn gzputs_error() {
+    const CONTENT: &[u8] = b"example\0";
+
+    // gzputs on a null file handle should return -1.
+    assert_eq!(
+        unsafe { gzputs(ptr::null_mut(), CONTENT.as_ptr().cast::<c_char>()) },
+        -1
+    );
+
+    // gzputs on a read-only file handle should return -1.
+    let file = unsafe { gzdopen(-2, CString::new("r").unwrap().as_ptr()) };
+    assert!(!file.is_null());
+    assert_eq!(
+        unsafe { gzputs(ptr::null_mut(), CONTENT.as_ptr().cast::<c_char>()) },
+        -1
+    );
+    assert_eq!(unsafe { gzclose(file) }, Z_ERRNO);
+}
+
 // Get the size in bytes of a file.
 //
 // # Returns


Reviews:
user: login: folkertdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T22:22:16Z
user: login: brian-pane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T03:53:30Z

Review comments:
diff_hunk: @@ -1738,6 +1738,111 @@ pub unsafe extern "C-unwind" fn gzoffset(file: gzFile) -> z_off_t {
     }
 }
 
+/// Compress and write `c`, converted to an unsigned 8-bit char, into `file`.
+///
+/// # Returns
+///
+///  - The value that was written, on success.
+///  - `-1` on error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+pub unsafe extern "C-unwind" fn gzputc(file: gzFile, c: c_int) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    /* FIXME: Uncomment when seek support is implemented.
+    // Check for seek request.
+    if state.seek {
+        state.seek = false;
+        if gz_zero(state, state.skip) == -1 {
+            return -1;
+        }
+    }
+     */
+
+    // Try writing to input buffer for speed (state.input == null if buffer not initialized).
+    if !state.input.is_null() {
+        if state.stream.avail_in == 0 {
+            state.stream.next_in = state.input;
+        }
+        let have = unsafe {
+            state
+                .stream
+                .next_in
+                .add(state.stream.avail_in as usize)
+                .offset_from(state.input)
+        } as usize;
+        if have < state.in_size {
+            // Safety: `input` has `in_size` bytes, and `have` < `in_size`.
+            unsafe { *state.input.add(have) = c as u8 };
+            state.stream.avail_in += 1;
+            state.pos += 1;
+            return c & 0xff;
+        }
+    }
+
+    // No room in buffer or not initialized, use gz_write.
+    let buf = [c as u8];
+    // Safety: We have confirmed that `state` is valid, and `buf` contains 1 readable byte of data.
+    if unsafe { gz_write(state, buf.as_ptr().cast::<c_void>(), 1) } != 1 {
+        return -1;
+    }
+    c & 0xff
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: ```suggestion
    match unsafe { gz_write(state, buf.as_ptr().cast::<c_void>(), 1) } {
        1 => c & 0xff,
        _ => -1,
    }
```
created_at: 2025-04-23T22:16:35Z
author_association: MEMBER
diff_hunk: @@ -1738,6 +1738,111 @@ pub unsafe extern "C-unwind" fn gzoffset(file: gzFile) -> z_off_t {
     }
 }
 
+/// Compress and write `c`, converted to an unsigned 8-bit char, into `file`.
+///
+/// # Returns
+///
+///  - The value that was written, on success.
+///  - `-1` on error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+pub unsafe extern "C-unwind" fn gzputc(file: gzFile, c: c_int) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    /* FIXME: Uncomment when seek support is implemented.
+    // Check for seek request.
+    if state.seek {
+        state.seek = false;
+        if gz_zero(state, state.skip) == -1 {
+            return -1;
+        }
+    }
+     */
+
+    // Try writing to input buffer for speed (state.input == null if buffer not initialized).
+    if !state.input.is_null() {
+        if state.stream.avail_in == 0 {
+            state.stream.next_in = state.input;
+        }
+        let have = unsafe {
+            state
+                .stream
+                .next_in
+                .add(state.stream.avail_in as usize)
+                .offset_from(state.input)
+        } as usize;
+        if have < state.in_size {
+            // Safety: `input` has `in_size` bytes, and `have` < `in_size`.
+            unsafe { *state.input.add(have) = c as u8 };
+            state.stream.avail_in += 1;
+            state.pos += 1;
+            return c & 0xff;
+        }
+    }
+
+    // No room in buffer or not initialized, use gz_write.
+    let buf = [c as u8];
+    // Safety: We have confirmed that `state` is valid, and `buf` contains 1 readable byte of data.
+    if unsafe { gz_write(state, buf.as_ptr().cast::<c_void>(), 1) } != 1 {
+        return -1;
+    }
+    c & 0xff
+}
+
+/// Compress and write the given null-terminated string `s` to file, excluding
+/// the terminating null character.
+///
+/// # Returns
+///
+/// - the number of characters written, on success.
+/// - `-1` in case of error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+/// - `s` must point to a null-terminated C string.
+pub unsafe extern "C-unwind" fn gzputs(file: gzFile, s: *const c_char) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    if s.is_null() {
+        return -1;
+    }
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    // Write string.
+    let len = unsafe { libc::strlen(s) };
+    if c_int::try_from(len).is_err() {
+        unsafe {
+            gz_error(
+                state,
+                Some((Z_STREAM_ERROR, "string length does not fit in int")),
+            )
+        };
+        return -1;
+    }
+    let put = unsafe { gz_write(state, s.cast::<c_void>(), len) };
+    if put < len as _ {
+        -1
+    } else {
+        len as _
+    }
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: ```suggestion
    match put.cmp(&len) {
        Ordering::Less => -1,
        Ordering::Equal | Ordering::Greater => len as _,
    }
```
created_at: 2025-04-23T22:19:28Z
author_association: MEMBER
diff_hunk: @@ -1738,6 +1738,111 @@ pub unsafe extern "C-unwind" fn gzoffset(file: gzFile) -> z_off_t {
     }
 }
 
+/// Compress and write `c`, converted to an unsigned 8-bit char, into `file`.
+///
+/// # Returns
+///
+///  - The value that was written, on success.
+///  - `-1` on error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+pub unsafe extern "C-unwind" fn gzputc(file: gzFile, c: c_int) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    /* FIXME: Uncomment when seek support is implemented.
+    // Check for seek request.
+    if state.seek {
+        state.seek = false;
+        if gz_zero(state, state.skip) == -1 {
+            return -1;
+        }
+    }
+     */
+
+    // Try writing to input buffer for speed (state.input == null if buffer not initialized).
+    if !state.input.is_null() {
+        if state.stream.avail_in == 0 {
+            state.stream.next_in = state.input;
+        }
+        let have = unsafe {
+            state
+                .stream
+                .next_in
+                .add(state.stream.avail_in as usize)
+                .offset_from(state.input)
+        } as usize;
+        if have < state.in_size {
+            // Safety: `input` has `in_size` bytes, and `have` < `in_size`.
+            unsafe { *state.input.add(have) = c as u8 };
+            state.stream.avail_in += 1;
+            state.pos += 1;
+            return c & 0xff;
+        }
+    }
+
+    // No room in buffer or not initialized, use gz_write.
+    let buf = [c as u8];
+    // Safety: We have confirmed that `state` is valid, and `buf` contains 1 readable byte of data.
+    if unsafe { gz_write(state, buf.as_ptr().cast::<c_void>(), 1) } != 1 {
+        return -1;
+    }
+    c & 0xff
+}
+
+/// Compress and write the given null-terminated string `s` to file, excluding
+/// the terminating null character.
+///
+/// # Returns
+///
+/// - the number of characters written, on success.
+/// - `-1` in case of error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+/// - `s` must point to a null-terminated C string.
+pub unsafe extern "C-unwind" fn gzputs(file: gzFile, s: *const c_char) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    if s.is_null() {
+        return -1;
+    }
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    // Write string.
+    let len = unsafe { libc::strlen(s) };
+    if c_int::try_from(len).is_err() {
+        unsafe {
+            gz_error(
+                state,
+                Some((Z_STREAM_ERROR, "string length does not fit in int")),
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: using `let msg = "..."` should reduce the linecount here
created_at: 2025-04-23T22:19:59Z
author_association: MEMBER
diff_hunk: @@ -1738,6 +1738,111 @@ pub unsafe extern "C-unwind" fn gzoffset(file: gzFile) -> z_off_t {
     }
 }
 
+/// Compress and write `c`, converted to an unsigned 8-bit char, into `file`.
+///
+/// # Returns
+///
+///  - The value that was written, on success.
+///  - `-1` on error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+pub unsafe extern "C-unwind" fn gzputc(file: gzFile, c: c_int) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    /* FIXME: Uncomment when seek support is implemented.
+    // Check for seek request.
+    if state.seek {
+        state.seek = false;
+        if gz_zero(state, state.skip) == -1 {
+            return -1;
+        }
+    }
+     */
+
+    // Try writing to input buffer for speed (state.input == null if buffer not initialized).
+    if !state.input.is_null() {
+        if state.stream.avail_in == 0 {
+            state.stream.next_in = state.input;
+        }
+        let have = unsafe {
+            state
+                .stream
+                .next_in
+                .add(state.stream.avail_in as usize)
+                .offset_from(state.input)
+        } as usize;
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: We already have this bit of code somewhere else right? Maybe this can be a method on `state`?
created_at: 2025-04-23T22:21:40Z
author_association: MEMBER
diff_hunk: @@ -1738,6 +1738,111 @@ pub unsafe extern "C-unwind" fn gzoffset(file: gzFile) -> z_off_t {
     }
 }
 
+/// Compress and write `c`, converted to an unsigned 8-bit char, into `file`.
+///
+/// # Returns
+///
+///  - The value that was written, on success.
+///  - `-1` on error.
+///
+/// # Safety
+///
+/// - `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+pub unsafe extern "C-unwind" fn gzputc(file: gzFile, c: c_int) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return -1;
+    };
+
+    // Check that we're writing and that there's no error.
+    if state.mode != GzMode::GZ_WRITE || state.err != Z_OK {
+        return -1;
+    }
+
+    /* FIXME: Uncomment when seek support is implemented.
+    // Check for seek request.
+    if state.seek {
+        state.seek = false;
+        if gz_zero(state, state.skip) == -1 {
+            return -1;
+        }
+    }
+     */
+
+    // Try writing to input buffer for speed (state.input == null if buffer not initialized).
+    if !state.input.is_null() {
+        if state.stream.avail_in == 0 {
+            state.stream.next_in = state.input;
+        }
+        let have = unsafe {
+            state
+                .stream
+                .next_in
+                .add(state.stream.avail_in as usize)
+                .offset_from(state.input)
+        } as usize;
path: libz-rs-sys/src/gz.rs
user: login: brian-pane
body: I just pushed an update where this is encapsulated in a function. I made a small change to `gz_write` to simplify the safety conditions for this function. Let me know if the safety comments make sense.
created_at: 2025-04-24T03:53:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-23T19:27:06Z
body: ## [Codecov](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) Report
Attention: Patch coverage is `90.66667%` with `7 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | Patch % | Lines |
|---|---|---|
| [libz-rs-sys/src/gz.rs](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351?src=pr&el=tree&filepath=libz-rs-sys%2Fsrc%2Fgz.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation#diff-bGliei1ycy1zeXMvc3JjL2d6LnJz) | 90.66% | [7 Missing :warning: ](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) |

| [Flag](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | Coverage Δ | |
|---|---|---|
| [fuzz-compress](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `?` | |
| [fuzz-decompress](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `?` | |
| [test-aarch64-apple-darwin](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `88.99% <90.66%> (+0.39%)` | :arrow_up: |
| [test-x86_64-apple-darwin](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `86.99% <90.66%> (-0.08%)` | :arrow_down: |
| [test-x86_64-unknown-linux-gnu](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `90.20% <90.66%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | Coverage Δ | |
|---|---|---|
| [libz-rs-sys/src/gz.rs](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351?src=pr&el=tree&filepath=libz-rs-sys%2Fsrc%2Fgz.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation#diff-bGliei1ycy1zeXMvc3JjL2d6LnJz) | `93.33% <90.66%> (+2.17%)` | :arrow_up: |

... and [6 files with indirect coverage changes](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/351/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation)

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: brian-pane
created_at: 2025-04-23T19:36:43Z
body: There are a couple of test coverage gaps that I want to fix, and then I'll move this PR out of draft state.
author_association: CONTRIBUTOR