Repository: trifectatechfoundation/zlib-rs
Pull request #341: Implement gzdirect
Author: login: brian-pane
Created: 2025-04-13T23:47:19Z | Merged: 2025-04-16T20:46:38Z



Diff:
diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml
index 168da013..cc083c9e 100644
--- a/.github/workflows/checks.yaml
+++ b/.github/workflows/checks.yaml
@@ -429,7 +429,7 @@ jobs:
       - name: Test gz logic with miri
         run: "cargo +nightly miri nextest run -j4 -p libz-rs-sys --target ${{ matrix.target }} --features=gz"
         env:
-          MIRIFLAGS: "-Zmiri-tree-borrows"
+          MIRIFLAGS: "-Zmiri-tree-borrows -Zmiri-disable-isolation"
 
   run-flate2-test-suite:
     name: run flate2 test suite
diff --git a/libz-rs-sys/src/gz.rs b/libz-rs-sys/src/gz.rs
index 1629f5f2..bb232e26 100644
--- a/libz-rs-sys/src/gz.rs
+++ b/libz-rs-sys/src/gz.rs
@@ -3,10 +3,12 @@
 use zlib_rs::allocate::*;
 pub use zlib_rs::c_api::*;
 
+use crate::{deflateEnd, inflateEnd, inflateInit2, inflateReset};
 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;
+use zlib_rs::MAX_WBITS;
 
 /// 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
@@ -37,11 +39,12 @@ struct GzState {
     mode: GzMode,
     fd: c_int, // file descriptor
     source: Source,
-    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
+    want: usize,     // requested buffer size, default is GZBUFSIZE
+    input: *mut u8,  // input buffer (double-sized when writing)
+    in_size: usize,  // size of *input
+    output: *mut u8, // output buffer (double-sized when reading)
+    out_size: usize, // size of *output
+    direct: bool,    // true in pass-through mode, false if processing gzip data
 
     // Fields used just for reading
     how: How,
@@ -59,10 +62,11 @@ struct GzState {
     seek: bool, // whether a seek request is pending
 
     // Error information
-    err: c_int, // last error (0 if no error)
+    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
+    // zlib inflate or deflate stream
+    stream: z_stream,
 }
 
 impl GzState {
@@ -111,12 +115,10 @@ enum GzMode {
 
 // gzip read strategies
 // NOTE: These values match what zlib-ng uses.
+#[derive(Debug, PartialEq, Eq)]
 enum How {
     Look = 0, // look for a gzip header
-    // FIXME Remove "allow(dead_code)" when code using COPY & GZIP is added.
-    #[allow(dead_code)]
     Copy = 1, // copy input directly
-    #[allow(dead_code)]
     Gzip = 2, // decompress a gzip stream
 }
 
@@ -195,7 +197,8 @@ unsafe fn gzopen_help(source: Source, mode: *const c_char) -> gzFile {
     // has the right size and alignment to be used as a GzState. And because the
     // allocator zeroes the allocated space, all the GzState fields are initialized.
     let state = unsafe { state.cast::<GzState>().as_mut() };
-    state.size = 0;
+    state.in_size = 0;
+    state.out_size = 0;
     state.want = GZBUFSIZE;
     state.msg = ptr::null();
 
@@ -204,6 +207,11 @@ unsafe fn gzopen_help(source: Source, mode: *const c_char) -> gzFile {
     state.strategy = Strategy::Default;
     state.direct = false;
 
+    state.stream = z_stream::default();
+    state.stream.zalloc = Some(ALLOCATOR.zalloc);
+    state.stream.zfree = Some(ALLOCATOR.zfree);
+    state.stream.opaque = ALLOCATOR.opaque;
+
     let mode = unsafe { CStr::from_ptr(mode) };
     let Ok((exclusive, cloexec)) = state.configure(mode.to_bytes()) else {
         // Safety: state is a valid pointer allocated in this function and not used after freeing
@@ -357,8 +365,7 @@ fn gz_reset(state: &mut GzState) {
                         // Safety: It is valid to pass a null msg pointer to `gz_error`.
     unsafe { gz_error(state, None) }; // clear error status
     state.pos = 0; // no uncompressed data yet
-                   // FIXME add once the deflate state is implemented:
-                   // state->strm.avail_in = 0;       /* no input data yet */
+    state.stream.avail_in = 0; // no input data yet
 }
 
 // Set the error message for a gzip stream, and deallocate any
@@ -431,6 +438,9 @@ unsafe fn gz_error(state: &mut GzState, err_msg: Option<(c_int, &str)>) {
 //   using `ALLOCATOR`.
 // - The caller must not use the `state` after passing it to this function.
 unsafe fn free_state(state: *mut GzState) {
+    if state.is_null() {
+        return;
+    }
     // Safety: `deallocate_cstr` accepts null pointers or C strings, and in this
     // module we use only `ALLOCATOR` to allocate strings assigned to these fields.
     unsafe {
@@ -440,10 +450,34 @@ unsafe fn free_state(state: *mut GzState) {
         }
         deallocate_cstr((*state).msg.cast_mut());
     }
+    // Safety: state is a valid GzState, and free_buffers checks for null
+    // input and output pointers internally.
+    unsafe { free_buffers(state.as_mut().unwrap()) };
+
     // Safety: The caller has ensured that `state` was allocated using `ALLOCATOR`.
-    unsafe {
-        ALLOCATOR.deallocate::<GzState>(state, 1);
+    unsafe { ALLOCATOR.deallocate(state, 1) };
+}
+
+// Deallocate the input and output buffers in a GzState.
+//
+// # Safety
+//
+// * `state` must have been obtained from [`gzopen`] or [`gzdopen`].
+unsafe fn free_buffers(state: &mut GzState) {
+    if !state.input.is_null() {
+        // Safety: state.input is always allocated using ALLOCATOR, and
+        // its allocation size is stored in state.in_size.
+        unsafe { ALLOCATOR.deallocate(state.input, state.in_size) };
+        state.input = ptr::null_mut();
+    }
+    state.in_size = 0;
+    if !state.output.is_null() {
+        // Safety: state.input is always allocated using ALLOCATOR, and
+        // its allocation size is stored in state.in_size.
+        unsafe { ALLOCATOR.deallocate(state.output, state.out_size) };
+        state.output = ptr::null_mut();
     }
+    state.out_size = 0;
 }
 
 // Free a string that was allocated with `ALLOCATOR`
@@ -478,20 +512,90 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            GzMode::GZ_WRITE | GzMode::GZ_APPEND | GzMode::GZ_NONE => gzclose_w(state),
+        },
+        None => Z_STREAM_ERROR,
     };
 
-    // FIXME: once read/write support is added, clean up internal buffers
+    // Note: In zlib-ng, the gzclose_r and gzclose_w functions deallocate the state
+    // object themselves. Here, since those functions take references to the state, the
+    // deallocation cannot safely happen until the references are out of scope. Therefore
+    // the free_state call handles the deallocation using the raw pointer to the state.
+    unsafe { free_state(file.cast::<GzState>()) };
+
+    ret
+}
 
-    let err = unsafe { libc::close(state.fd) };
-    unsafe { free_state(state) };
-    match err {
-        0 => Z_OK,
+// Close a gzip file that was opened for reading.
+//
+// # Returns
+//
+// * Z_OK if `state` has no outstanding error and the file is closed successfully.
+// * A Z_ error code if the `state` is null or the file close operation fails.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gzclose_r(state: &mut GzState) -> c_int {
+    if state.mode != GzMode::GZ_READ {
+        return Z_STREAM_ERROR;
+    }
+
+    // Free buffers and close the file
+    if state.in_size != 0 {
+        // Safety: state.stream was properly initialized as a z_stream in gzopen_help.
+        unsafe { inflateEnd(&mut state.stream as *mut z_stream) };
+    }
+    let err = match state.err {
+        Z_BUF_ERROR => Z_BUF_ERROR,
+        _ => Z_OK,
+    };
+    match unsafe { libc::close(state.fd) } {
+        0 => err,
         _ => Z_ERRNO,
     }
 }
 
+// Close a gzip file that was opened for writing.
+//
+// # Returns
+//
+// * Z_OK if `state` has no outstanding error and the file is closed successfully.
+// * A Z_ error code if the `state` is null or the file close operation fails.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen`] or [`gzdopen`].
+fn gzclose_w(state: &mut GzState) -> c_int {
+    if state.mode != GzMode::GZ_WRITE {
+        return Z_STREAM_ERROR;
+    }
+
+    let mut ret = Z_OK;
+
+    // Check for a pending seek request
+    if state.seek {
+        state.seek = false;
+        // FIXME add a call to gz_zero(state, state.skip) when write support is implemented.
+        ret = state.err;
+    }
+
+    // FIXME add a call go gz_comp(state, Z_FINISH) when write support is implemented.
+
+    if state.in_size != 0 && !state.direct {
+        // Safety: state.stream was properly initialized as a z_stream in gzopen_help.
+        unsafe { deflateEnd(&mut state.stream as *mut z_stream) };
+    }
+    if unsafe { libc::close(state.fd) } == -1 {
+        ret = Z_ERRNO;
+    }
+
+    ret
+}
+
 /// Retrieve the zlib error code and a human-readable string description of
 /// the most recent error on a gzip file stream.
 ///
@@ -607,6 +711,235 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        let _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // `gz_avail` attempts to read as much data as available from the underlying file
+        // into the input buffer. This will hopefully give us enough bytes to check for a
+        // gzip file header.
+        // Safety: The caller confirmed the validity of `state`.
+        if unsafe { gz_avail(state) }? == 0 {
+            return Ok(());
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Note: If we are reading a partially written gzip file, and all that is available to read is
+    // the first byte of the gzip magic number, we cannot tell whether what follows will be the
+    // rest of the gzip magic. For simplicity, we assume that the writer of a gzip file will
+    // write the header (or at least the magic number at the start of the header) atomically,
+    // so if our initial read found a single byte it is a sufficient indication that the file
+    // is not in gzip format.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return Ok(());
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    // FIXME: Add test coverage for this case (which may require the remaining decompression logic).
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return Ok(());
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy_nonoverlapping(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    Ok(())
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * `Ok(n)` on success, where `n` is the number of bytes available (`state.stream.avail_in`)
+// * `Err` on error
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> Result<usize, ()> {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return Err(());
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start. Note: The source and destination are
+            // within the same buffer, so this may be an overlapping copy.
+            unsafe {
+                ptr::copy(
+                    state.stream.next_in,
+                    state.input,
+                    state.stream.avail_in as usize,
+                )
+            };
+        }
+        let got = unsafe {
+            gz_load(
+                state,
+                state.input.add(state.stream.avail_in as usize),
+                state.in_size - state.stream.avail_in as usize,
+            )
+        }?;
+        state.stream.avail_in += got as uInt;
+        state.stream.next_in = state.input;
+    }
+    Ok(state.stream.avail_in as usize)
+}
+
+// Read data from `state`'s underlying file descriptor into a buffer.
+//
+// # Returns
+//
+// * `Ok(n)` on success, where `n` is the number of bytes read.
+// * `Err` on error
+//
+// # Arguments
+//
+// * `state` - gzip file handle.
+// * `buf` - address at which the data read from the file should be stored.
+// * `size` - number of bytes to read
+//
+// # Safety
+//
+// * `state` must have been properly initialized, e.g. by [`gzopen_help`].
+// * `buf` mut point to a writable block of at least `len` bytes.
+unsafe fn gz_load(state: &mut GzState, buf: *mut u8, len: usize) -> Result<usize, ()> {
+    let mut have = 0;
+    let mut ret = 0;
+    while have < len {
+        ret = unsafe { libc::read(state.fd, buf.add(have).cast::<_>(), (len - have) as _) };
+        if ret <= 0 {
+            break;
+        }
+        have += ret as usize;
+    }
+    if ret < 0 {
+        unsafe { gz_error(state, Some((Z_ERRNO, "read error"))) }; // FIXME implement `zstrerror`
+        return Err(());
+    }
+    if ret == 0 {
+        state.eof = true;
+    }
+    Ok(have)
+}
+
 // Create a deep copy of a C string using `ALLOCATOR`
 //
 // # Safety
@@ -667,6 +1000,19 @@ unsafe fn gz_strcat(strings: &[&str]) -> *mut c_char {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::ffi::CString;
+    use std::path::Path;
+
+    // Generate a file path relative to the project's root
+    fn crate_path(file: &str) -> String {
+        path(Path::new(env!("CARGO_MANIFEST_DIR")), file)
+    }
+
+    fn path(prefix: &Path, file: &str) -> String {
+        let mut path_buf = prefix.to_path_buf();
+        path_buf.push(file);
+        path_buf.as_path().to_str().unwrap().to_owned()
+    }
 
     #[test]
     fn test_configure() {
@@ -796,6 +1142,9 @@ mod tests {
     #[test]
     #[cfg_attr(not(target_os = "linux"), ignore = "lseek is not implemented")]
     fn test_gz_error() {
+        // gzerror(null) should return null.
+        assert!(unsafe { gzerror(ptr::null_mut(), ptr::null_mut()) }.is_null());
+
         // Open a gzip stream with an invalid file handle. Initially, no error
         // status should be set.
         let handle = unsafe { gzdopen(-2, c!(b"r\0")) };
@@ -964,4 +1313,102 @@ mod tests {
             assert_eq!(unsafe { gzclose(handle) }, Z_ERRNO);
         }
     }
+
+    #[test]
+    #[cfg_attr(not(target_os = "linux"), ignore = "lseek is not implemented")]
+    // Open a gzip file for reading. gzdirect should return 0.
+    fn test_gzdirect_gzip_file() {
+        let file = unsafe {
+            gzopen(
+                CString::new(crate_path("src/test-data/example.gz"))
+                    .unwrap()
+                    .as_ptr(),
+                CString::new("r").unwrap().as_ptr(),
+            )
+        };
+        assert!(!file.is_null());
+        // Set a smaller read batch size to exercise the buffer management code paths.
+        const FILE_SIZE: usize = 48; // size of test-data/example.gz
+        const BLOCK_SIZE: usize = 40;
+        unsafe { file.cast::<GzState>().as_mut().unwrap().want = BLOCK_SIZE };
+        assert_eq!(unsafe { gzdirect(file) }, 0);
+        // gzdirect should have pulled the first `BLOCK_SIZE` bytes of the file into `file`'s internal input buffer.
+        assert_eq!(unsafe { file.cast::<GzState>().as_ref().unwrap().have }, 0);
+        assert_eq!(
+            unsafe { file.cast::<GzState>().as_ref().unwrap().stream.avail_in },
+            BLOCK_SIZE as uInt
+        );
+        // Consume some of the buffered input and call gz_avail. It should move the remaining
+        // input to the front of the input buffer.
+        unsafe {
+            let state = file.cast::<GzState>().as_mut().unwrap();
+            const CONSUME: usize = 10;
+            state.stream.next_in = state.stream.next_in.add(CONSUME);
+            state.stream.avail_in -= CONSUME as uInt;
+            let expected_avail = BLOCK_SIZE - CONSUME + (FILE_SIZE - BLOCK_SIZE);
+            assert_eq!(gz_avail(state), Ok(expected_avail));
+            assert_eq!(state.stream.avail_in as usize, expected_avail);
+        };
+        assert_eq!(unsafe { gzclose(file) }, Z_OK);
+    }
+
+    // Open a non-gzip file for reading. gzdirect should return 1.
+    #[test]
+    #[cfg_attr(not(target_os = "linux"), ignore = "lseek is not implemented")]
+    fn test_gzdirect_non_gzip_file() {
+        let file = unsafe {
+            gzopen(
+                CString::new(crate_path("src/test-data/example.txt"))
+                    .unwrap()
+                    .as_ptr(),
+                CString::new("r").unwrap().as_ptr(),
+            )
+        };
+        assert!(!file.is_null());
+        assert_eq!(unsafe { gzdirect(file) }, 1);
+        // gzdirect should have pulled the entire contents of the file (which is smaller than
+        // GZBUFSIZE) into `file`'s internal output buffer.
+        assert_eq!(unsafe { file.cast::<GzState>().as_ref().unwrap().have }, 20);
+        assert_eq!(
+            unsafe { file.cast::<GzState>().as_ref().unwrap().stream.avail_in },
+            0
+        );
+        assert_eq!(unsafe { gzclose(file) }, Z_OK);
+
+        // Open a file containing only the gzip magic number. gzdirect should return 0.
+        let file = unsafe {
+            gzopen(
+                CString::new(crate_path("src/test-data/magic-only.gz"))
+                    .unwrap()
+                    .as_ptr(),
+                CString::new("r").unwrap().as_ptr(),
+            )
+        };
+        assert!(!file.is_null());
+        assert_eq!(unsafe { gzdirect(file) }, 0);
+        assert_eq!(unsafe { file.cast::<GzState>().as_ref().unwrap().have }, 0);
+        assert_eq!(
+            unsafe { file.cast::<GzState>().as_ref().unwrap().stream.avail_in },
+            2
+        );
+        assert_eq!(unsafe { gzclose(file) }, Z_OK);
+
+        // Open a file containing only the first byte of the gzip magic number. gzdirect should return 1.
+        let file = unsafe {
+            gzopen(
+                CString::new(crate_path("src/test-data/incomplete-magic.gz"))
+                    .unwrap()
+                    .as_ptr(),
+                CString::new("r").unwrap().as_ptr(),
+            )
+        };
+        assert!(!file.is_null());
+        assert_eq!(unsafe { gzdirect(file) }, 1);
+        assert_eq!(unsafe { file.cast::<GzState>().as_ref().unwrap().have }, 1);
+        assert_eq!(
+            unsafe { file.cast::<GzState>().as_ref().unwrap().stream.avail_in },
+            0
+        );
+        assert_eq!(unsafe { gzclose(file) }, Z_OK);
+    }
 }
diff --git a/libz-rs-sys/src/test-data/example.gz b/libz-rs-sys/src/test-data/example.gz
new file mode 100644
index 00000000..33ce25e2
Binary files /dev/null and b/libz-rs-sys/src/test-data/example.gz differ
diff --git a/libz-rs-sys/src/test-data/example.txt b/libz-rs-sys/src/test-data/example.txt
new file mode 100644
index 00000000..af8bfc3a
--- /dev/null
+++ b/libz-rs-sys/src/test-data/example.txt
@@ -0,0 +1 @@
+zlib-rs gzip example
\ No newline at end of file
diff --git a/libz-rs-sys/src/test-data/incomplete-magic.gz b/libz-rs-sys/src/test-data/incomplete-magic.gz
new file mode 100644
index 00000000..1d799497
--- /dev/null
+++ b/libz-rs-sys/src/test-data/incomplete-magic.gz
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/libz-rs-sys/src/test-data/magic-only.gz b/libz-rs-sys/src/test-data/magic-only.gz
new file mode 100644
index 00000000..324b9f74
--- /dev/null
+++ b/libz-rs-sys/src/test-data/magic-only.gz
@@ -0,0 +1 @@
+�
\ No newline at end of file
diff --git a/test-libz-rs-sys/src/gz.rs b/test-libz-rs-sys/src/gz.rs
index 016f9073..55875d8b 100644
--- a/test-libz-rs-sys/src/gz.rs
+++ b/test-libz-rs-sys/src/gz.rs
@@ -1,6 +1,6 @@
 use zlib_rs::c_api::*;
 
-use libz_rs_sys::{gzFile_s, gzclose, gzdopen, gzerror, gzopen};
+use libz_rs_sys::{gzFile_s, gzclose, gzdirect, gzdopen, gzerror, gzopen};
 
 use std::ffi::{c_char, c_int, CString};
 use std::path::Path;
@@ -26,7 +26,13 @@ macro_rules! test_open {
         let cpath = CString::new($path).unwrap();
         let cmode = CString::new($mode).unwrap();
         let handle = unsafe { gzopen(cpath.as_ptr(), cmode.as_ptr()) };
-        assert_eq!($should_succeed, !handle.is_null(), "gzopen({}, {})", $path, $mode);
+        assert_eq!(
+            $should_succeed,
+            !handle.is_null(),
+            "gzopen({}, {})",
+            $path,
+            $mode
+        );
         if !handle.is_null() {
             assert_eq!(unsafe { gzclose(handle) }, Z_OK, "gzclose({}) error", $path);
         }
@@ -38,7 +44,13 @@ macro_rules! test_fdopen {
     ($fd:expr, $mode:expr, $should_succeed:expr) => {
         let cmode = CString::new($mode).unwrap();
         let handle = unsafe { gzdopen($fd, cmode.as_ptr()) };
-        assert_eq!($should_succeed, !handle.is_null(), "gzdopen({}, {})", $fd, $mode);
+        assert_eq!(
+            $should_succeed,
+            !handle.is_null(),
+            "gzdopen({}, {})",
+            $fd,
+            $mode
+        );
         if !handle.is_null() {
             assert_eq!(unsafe { gzclose(handle) }, Z_OK, "gzclose({}) error", $fd);
         }
@@ -133,3 +145,86 @@ fn gz_error_access() {
     assert_eq!(unsafe { *err }, 0 as c_char);
     assert_eq!(unsafe { gzclose(handle) }, Z_OK);
 }
+
+#[test]
+fn gz_direct_write() {
+    // Create a temporary directory that will be automatically removed when
+    // temp_dir goes out of scope.
+    let temp_dir_path = if cfg!(target_os = "wasi") {
+        std::path::PathBuf::from("/tmp/")
+    } else {
+        std::env::temp_dir()
+    };
+    let temp_dir = tempfile::TempDir::new_in(temp_dir_path).unwrap();
+    let temp_path = temp_dir.path();
+
+    // Test write and append modes, where direct mode is specified by the call to `gzopen`.
+    for mode in ["w", "a"] {
+        // Open a new file without the "T" mode flag. It should be in non-direct mode.
+        let file = unsafe {
+            gzopen(
+                CString::new(path(temp_path, "compressed.gz"))
+                    .unwrap()
+                    .as_ptr(),
+                CString::new(mode).unwrap().as_ptr(),
+            )
+        };
+        assert!(!file.is_null());
+        assert_eq!(unsafe { gzdirect(file) }, 0);
+        assert_eq!(unsafe { gzclose(file) }, Z_OK);
+
+        // Open a new file with the "T" mode flag. It should be in direct mode.
+        let file = unsafe {
+            gzopen(
+                CString::new(path(temp_path, "direct.gz")).unwrap().as_ptr(),
+                CString::new("T".to_owned() + mode).unwrap().as_ptr(),
+            )
+        };
+        assert!(!file.is_null());
+        assert_eq!(unsafe { gzdirect(file) }, 1);
+        assert_eq!(unsafe { gzclose(file) }, Z_OK);
+    }
+}
+
+#[test]
+fn gz_direct_read() {
+    // gzdirect(null) should return 0.
+    assert_eq!(unsafe { gzdirect(ptr::null_mut()) }, 0);
+
+    // Open a gzip file for reading. gzdirect should return 0.
+    let file = unsafe {
+        gzopen(
+            CString::new(crate_path("src/test-data/issue-109.gz"))
+                .unwrap()
+                .as_ptr(),
+            CString::new("r").unwrap().as_ptr(),
+        )
+    };
+    assert!(!file.is_null());
+    assert_eq!(unsafe { gzdirect(file) }, 0);
+    assert_eq!(unsafe { gzclose(file) }, Z_OK);
+
+    // Open a non-gzip file for reading. gzdirect should return 1.
+    let file = unsafe {
+        gzopen(
+            CString::new(crate_path("src/test-data/issue-169.js"))
+                .unwrap()
+                .as_ptr(),
+            CString::new("r").unwrap().as_ptr(),
+        )
+    };
+    assert!(!file.is_null());
+    assert_eq!(unsafe { gzdirect(file) }, 1);
+    assert_eq!(unsafe { gzclose(file) }, Z_OK);
+
+    // Open a gzip stream from an invalid file descriptor. gzdirect should return 1, but
+    // it should cause an error condition to be set within the stream.
+    let file = unsafe { gzdopen(-2, CString::new("r").unwrap().as_ptr()) };
+    assert!(!file.is_null());
+    assert_eq!(unsafe { gzdirect(file) }, 1);
+    let mut err = Z_OK;
+    let msg = unsafe { gzerror(file, &mut err as *mut c_int) };
+    assert!(!msg.is_null());
+    assert_eq!(err, Z_ERRNO);
+    assert_eq!(unsafe { gzclose(file) }, Z_ERRNO);
+}


Reviews:
user: login: folkertdev
body: Mostly looks good

- I think it'll be more convenient to use `Result` for private functions that return "success or failure"
- there are some blocks that are not covered by tests. This is mostly just fyi, and can be fixed later, but if it's easy to fix now, all the better.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T09:00:34Z
user: login: brian-pane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T22:49:43Z
user: login: folkertdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T09:13:58Z
user: login: folkertdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T09:22:12Z
user: login: brian-pane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:11:05Z
user: login: brian-pane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:07:51Z
user: login: brian-pane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:08:27Z
user: login: brian-pane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:33:15Z
user: login: brian-pane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:54:02Z
user: login: folkertdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T19:40:38Z
user: login: folkertdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T19:42:39Z
user: login: folkertdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T19:45:51Z

Review comments:
diff_hunk: @@ -607,6 +707,212 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * 0 on success
+// * -1 on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> c_int {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return -1;
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        if unsafe { gz_avail(state) } == -1 {
+            return -1;
+        }
+        if state.stream.avail_in == 0 {
+            return 0;
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return 0;
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return 0;
+    }
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: this block is not currently hit by tests
created_at: 2025-04-14T08:45:02Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,212 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * 0 on success
+// * -1 on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> c_int {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return -1;
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        if unsafe { gz_avail(state) } == -1 {
+            return -1;
+        }
+        if state.stream.avail_in == 0 {
+            return 0;
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return 0;
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return 0;
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy_nonoverlapping(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    0
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * -1 if an error occurs.
+// * 0 otherwise.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> c_int {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return -1;
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start.
+            unsafe {
+                ptr::copy_nonoverlapping(
+                    state.stream.next_in,
+                    state.input,
+                    state.stream.avail_in as usize,
+                )
+            };
+        }
+        let Ok(got) = (unsafe {
+            gz_load(
+                state,
+                state.input.add(state.stream.avail_in as usize),
+                state.in_size - state.stream.avail_in as usize,
+            )
+        }) else {
+            return -1;
+        };
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: if you make this function return a `Result<(), ()>`, then the try operator `?` can be used to get rid of this let-else.
created_at: 2025-04-14T08:47:36Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,212 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * 0 on success
+// * -1 on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> c_int {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return -1;
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        if unsafe { gz_avail(state) } == -1 {
+            return -1;
+        }
+        if state.stream.avail_in == 0 {
+            return 0;
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return 0;
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return 0;
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy_nonoverlapping(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    0
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * -1 if an error occurs.
+// * 0 otherwise.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> c_int {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return -1;
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start.
+            unsafe {
+                ptr::copy_nonoverlapping(
+                    state.stream.next_in,
+                    state.input,
+                    state.stream.avail_in as usize,
+                )
+            };
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: this is not hit by tests
created_at: 2025-04-14T08:47:49Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,212 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * 0 on success
+// * -1 on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> c_int {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return -1;
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        if unsafe { gz_avail(state) } == -1 {
+            return -1;
+        }
+        if state.stream.avail_in == 0 {
+            return 0;
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return 0;
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return 0;
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy_nonoverlapping(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    0
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * -1 if an error occurs.
+// * 0 otherwise.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> c_int {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return -1;
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start.
+            unsafe {
+                ptr::copy_nonoverlapping(
+                    state.stream.next_in,
+                    state.input,
+                    state.stream.avail_in as usize,
+                )
+            };
+        }
+        let Ok(got) = (unsafe {
+            gz_load(
+                state,
+                state.input.add(state.stream.avail_in as usize),
+                state.in_size - state.stream.avail_in as usize,
+            )
+        }) else {
+            return -1;
+        };
+        state.stream.avail_in += got as uInt;
+        state.stream.next_in = state.input;
+    }
+    0
+}
+
+unsafe fn gz_load(state: &mut GzState, buf: *mut u8, len: size_t) -> Result<size_t, ()> {
+    let mut have = 0;
+    let mut ret = 0;
+    while have < len {
+        ret = unsafe { libc::read(state.fd, buf.add(have).cast::<_>(), (len - have) as _) };
+        if ret <= 0 {
+            break;
+        }
+        have += ret as size_t;
+    }
+    if ret < 0 {
+        unsafe { gz_error(state, Some((Z_ERRNO, "read error"))) }; // FIXME implement `zstrerror`
+        return Err(());
+    }
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: this is not hit by tests
created_at: 2025-04-14T08:49:00Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,212 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * 0 on success
+// * -1 on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> c_int {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return -1;
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        if unsafe { gz_avail(state) } == -1 {
+            return -1;
+        }
+        if state.stream.avail_in == 0 {
+            return 0;
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return 0;
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return 0;
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy_nonoverlapping(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    0
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * -1 if an error occurs.
+// * 0 otherwise.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> c_int {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return -1;
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start.
+            unsafe {
+                ptr::copy_nonoverlapping(
+                    state.stream.next_in,
+                    state.input,
+                    state.stream.avail_in as usize,
+                )
+            };
+        }
+        let Ok(got) = (unsafe {
+            gz_load(
+                state,
+                state.input.add(state.stream.avail_in as usize),
+                state.in_size - state.stream.avail_in as usize,
+            )
+        }) else {
+            return -1;
+        };
+        state.stream.avail_in += got as uInt;
+        state.stream.next_in = state.input;
+    }
+    0
+}
+
+unsafe fn gz_load(state: &mut GzState, buf: *mut u8, len: size_t) -> Result<size_t, ()> {
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: `size_t` is usually just `usize`, because it's not in the public api, could `usize` be used here instead?
created_at: 2025-04-14T08:53:42Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,212 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * 0 on success
+// * -1 on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> c_int {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return -1;
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return -1;
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        if unsafe { gz_avail(state) } == -1 {
+            return -1;
+        }
+        if state.stream.avail_in == 0 {
+            return 0;
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return 0;
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return 0;
+    }
path: libz-rs-sys/src/gz.rs
user: login: brian-pane
body: I was able to cover the other blocks with new tests. This one got messy enough that I put in a FIXME comment for now. The cleanest way to get the `GzState` into the proper state to trigger this block is probably to finish implementing the read+decompress support, and then create a gzip file with junk content at the end.
created_at: 2025-04-14T22:49:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -478,20 +512,86 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            _ => gzclose_w(state),
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: ```suggestion
            GzMode::GZ_WRITE | GzMode::GzAppend => gzclose_w(state),
```

when it's that easy to make the match exhaustive, might as well do it.
created_at: 2025-04-16T08:39:52Z
author_association: MEMBER
diff_hunk: @@ -478,20 +512,86 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            _ => gzclose_w(state),
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: I also like the previous version with `let-else` a bit better personally, just because it reduces indentation and visual noise.
created_at: 2025-04-16T08:40:53Z
author_association: MEMBER
diff_hunk: @@ -478,20 +512,86 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            _ => gzclose_w(state),
+        },
+        None => Z_STREAM_ERROR,
     };
 
-    // FIXME: once read/write support is added, clean up internal buffers
+    unsafe { free_state(file.cast::<GzState>()) };
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: This `free_state` is not in the zlib-ng version I think?

```c
int Z_EXPORT PREFIX(gzclose)(gzFile file) {
#ifndef NO_GZCOMPRESS
    gz_state *state;

    if (file == NULL)
        return Z_STREAM_ERROR;
    state = (gz_state *)file;

    return state->mode == GZ_READ ? PREFIX(gzclose_r)(file) : PREFIX(gzclose_w)(file);
#else
    return PREFIX(gzclose_r)(file);
#endif
}
```

So why do we need it? (if there is a good reason, please add a comment for future readers)
created_at: 2025-04-16T08:43:10Z
author_association: MEMBER
diff_hunk: @@ -478,20 +512,86 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            _ => gzclose_w(state),
+        },
+        None => Z_STREAM_ERROR,
     };
 
-    // FIXME: once read/write support is added, clean up internal buffers
+    unsafe { free_state(file.cast::<GzState>()) };
+
+    ret
+}
 
-    let err = unsafe { libc::close(state.fd) };
-    unsafe { free_state(state) };
-    match err {
-        0 => Z_OK,
+// Close a gzip file that was opened for reading.
+//
+// # Returns
+//
+// * Z_OK if `state` has no outstanding error and the file is closed successfully.
+// * A Z_ error code if the `state` is null or the file close operation fails.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gzclose_r(state: &mut GzState) -> c_int {
+    if state.mode != GzMode::GZ_READ {
+        return Z_STREAM_ERROR;
+    }
+
+    // Free buffers and close the file
+    if state.in_size != 0 {
+        // Safety: state.stream was properly initialized as a z_stream in gzopen_help.
+        unsafe { inflateEnd(&mut state.stream as *mut z_stream) };
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: please also leave a note here for why the logic deviates (missing state freeing logic) from zlib-ng
created_at: 2025-04-16T08:45:35Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: ```suggestion
        let _ = unsafe { gz_look(state) };
```

This is more common I think. Without the `let` it looks a bit weird.
created_at: 2025-04-16T08:47:53Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: where did the `state->size == 0` check go here?
created_at: 2025-04-16T08:57:50Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
+        if state.stream.avail_in == 0 {
+            return Ok(());
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: zlib-ng has a larger comment here with some helpful information
created_at: 2025-04-16T09:00:09Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: ```suggestion
        let _bytes_available = gz_avail(state)?;
```
created_at: 2025-04-16T09:02:46Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
+        if state.stream.avail_in == 0 {
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: this could use the return value from `gz_avail`. Let's do that in this case actually, so you get `if gz_avail(state)? == 0` and so on.
created_at: 2025-04-16T09:04:33Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: `gz_avail` is not a great name, so I think a short comment on what it does here would be helpful
created_at: 2025-04-16T09:05:10Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
+        if state.stream.avail_in == 0 {
+            return Ok(());
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return Ok(());
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    // FIXME: Add test coverage for this case (which may require the remaining decompression logic).
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return Ok(());
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: can these ever alias each other? I don't think they can given our safety assumptions, in which case
```suggestion
        ptr::copy_nonoverlapping(
            state.stream.next_in,
            state.output,
            state.stream.avail_in as usize,
        )
```
created_at: 2025-04-16T09:09:46Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
+        if state.stream.avail_in == 0 {
+            return Ok(());
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return Ok(());
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    // FIXME: Add test coverage for this case (which may require the remaining decompression logic).
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return Ok(());
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    Ok(())
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * `Ok(n)` on success, where `n` is the number of bytes available (`state.stream.avail_in`)
+// * `Err` on error
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> Result<usize, ()> {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return Err(());
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start.
+            unsafe {
+                ptr::copy(
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: I suspect also here 
```suggestion
                ptr::copy_nonoverlapping(
```
created_at: 2025-04-16T09:10:14Z
author_association: MEMBER
diff_hunk: @@ -964,4 +1302,112 @@ mod tests {
             assert_eq!(unsafe { gzclose(handle) }, Z_ERRNO);
         }
     }
+
+    #[test]
+    #[cfg_attr(not(target_os = "linux"), ignore = "lseek is not implemented")]
+    fn test_gzdirect() {
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: I think this should be (at least?) 3 tests. If you break them up, the test name can be a bit more descriptive (what exactly are we testing here) and the comment that is above the different sections now can become a doc comment on the test function.
created_at: 2025-04-16T09:11:47Z
author_association: MEMBER
diff_hunk: @@ -964,4 +1302,112 @@ mod tests {
             assert_eq!(unsafe { gzclose(handle) }, Z_ERRNO);
         }
     }
+
+    #[test]
+    #[cfg_attr(not(target_os = "linux"), ignore = "lseek is not implemented")]
+    fn test_gzdirect() {
+        // gzdirect(null) should return 0.
+        assert_eq!(unsafe { gzdirect(ptr::null_mut()) }, 0);
+
+        // Open a gzip stream from an invalid file descriptor. gzdirect should return 1, but
+        // it should cause an error condition to be set within the stream.
+        let file = unsafe { gzdopen(-2, CString::new("r").unwrap().as_ptr()) };
+        assert!(!file.is_null());
+        assert_eq!(unsafe { gzdirect(file) }, 1);
+        let mut err = Z_OK;
+        let msg = unsafe { gzerror(file, &mut err as *mut c_int) };
+        assert!(!msg.is_null());
+        assert_eq!(err, Z_ERRNO);
+        assert_eq!(unsafe { gzclose(file) }, Z_ERRNO);
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: this block can go into `test-libz-rs-sys` right? 
created_at: 2025-04-16T09:17:16Z
author_association: MEMBER
diff_hunk: @@ -478,20 +512,86 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            _ => gzclose_w(state),
+        },
+        None => Z_STREAM_ERROR,
     };
 
-    // FIXME: once read/write support is added, clean up internal buffers
+    unsafe { free_state(file.cast::<GzState>()) };
+
+    ret
+}
 
-    let err = unsafe { libc::close(state.fd) };
-    unsafe { free_state(state) };
-    match err {
-        0 => Z_OK,
+// Close a gzip file that was opened for reading.
+//
+// # Returns
+//
+// * Z_OK if `state` has no outstanding error and the file is closed successfully.
+// * A Z_ error code if the `state` is null or the file close operation fails.
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gzclose_r(state: &mut GzState) -> c_int {
+    if state.mode != GzMode::GZ_READ {
+        return Z_STREAM_ERROR;
+    }
+
+    // Free buffers and close the file
+    if state.in_size != 0 {
+        // Safety: state.stream was properly initialized as a z_stream in gzopen_help.
+        unsafe { inflateEnd(&mut state.stream as *mut z_stream) };
path: libz-rs-sys/src/gz.rs
user: login: brian-pane
body: I'll add a comment and post an update later today. The short answer is: the Rust versions of `gzclose_r` and `gzclose_w` receive a reference to the `GzState`, instead of a pointer. We can't deallocate that state inside those functions while the reference is in scope.
created_at: 2025-04-16T14:11:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
+        if state.stream.avail_in == 0 {
+            return Ok(());
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return Ok(());
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    // FIXME: Add test coverage for this case (which may require the remaining decompression logic).
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return Ok(());
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
path: libz-rs-sys/src/gz.rs
user: login: brian-pane
body: Yes, this one can use `copy_nonoverlapping`.
created_at: 2025-04-16T15:07:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
+        if state.stream.avail_in == 0 {
+            return Ok(());
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return Ok(());
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    // FIXME: Add test coverage for this case (which may require the remaining decompression logic).
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return Ok(());
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    Ok(())
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * `Ok(n)` on success, where `n` is the number of bytes available (`state.stream.avail_in`)
+// * `Err` on error
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> Result<usize, ()> {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return Err(());
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start.
+            unsafe {
+                ptr::copy(
path: libz-rs-sys/src/gz.rs
user: login: brian-pane
body: In this case, we're shifting data from the middle of a buffer to the front, so it may overlap.
created_at: 2025-04-16T15:08:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -478,20 +512,86 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            _ => gzclose_w(state),
path: libz-rs-sys/src/gz.rs
user: login: brian-pane
body: They key thing about this version, compared to the previous version, is that it puts the reference `state` within a nested scope. That reference's lifetime needs to end before the call to `free_state` or Miri will (correctly) raise an error when `free_state` deallocates the memory underlying a life reference. I'll add a comment about it.
created_at: 2025-04-16T15:33:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
path: libz-rs-sys/src/gz.rs
user: login: brian-pane
body: zlib-ng uses a single `size` field to store of the input and output buffer sizes, except that it's not really the buffer size, because of this:
```
    unsigned size;          /* buffer size, zero if not allocated yet */
    unsigned want;          /* requested buffer size, default is GZBUFSIZE */
    unsigned char *in;      /* input buffer (double-sized when writing) */
    unsigned char *out;     /* output buffer (double-sized when reading) */
```

One of the input or output buffer will be twice `state->size`, depending on the mode. In the Rust version, where we have to specify the exact buffer size when deallocating, I used `in_size` and `out_size` fields to reduce the potential sources of error.

Since the check for `state->size == 0` in `gz_look` is deciding whether it needs to allocate the I/O buffers, I used an equivalent check: `state.input.is_null()`. The check could alternately be `state.in_size == 0`.
created_at: 2025-04-16T15:54:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1353,8 +1349,13 @@ mod tests {
             assert_eq!(state.stream.avail_in as usize, expected_avail);
         };
         assert_eq!(unsafe { gzclose(file) }, Z_OK);
+    }
 
-        // Open a non-gzip file for reading. gzdirect should return 1.
+    // Open a non-gzip file for reading. gzdirect should return 1.
+    #[test]
+    #[cfg_attr(not(target_os = "linux"), ignore = "lseek is not implemented")]
+    // Open a gzip file for reading. gzdirect should return 0.
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: something weird happed with the comments here.
created_at: 2025-04-16T19:37:13Z
author_association: MEMBER
diff_hunk: @@ -478,20 +512,86 @@ unsafe fn deallocate_cstr(s: *mut c_char) {
 /// 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) = (unsafe { file.cast::<GzState>().as_mut() }) else {
-        return Z_STREAM_ERROR;
+    let ret = match unsafe { file.cast::<GzState>().as_mut() } {
+        Some(state) => match state.mode {
+            GzMode::GZ_READ => unsafe { gzclose_r(state) },
+            _ => gzclose_w(state),
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: Ah, nice. Where would we be without Miri.
created_at: 2025-04-16T19:42:39Z
author_association: MEMBER
diff_hunk: @@ -607,6 +707,228 @@ pub unsafe extern "C-unwind" fn gzeof(file: gzFile) -> c_int {
     state.past as _
 }
 
+/// Check whether `file` is in direct mode (reading or writing literal bytes without compression).
+///
+/// NOTE: If `gzdirect` is called immediately after [`gzopen`] or [`gzdopen`], it may allocate
+/// buffers internally to read the file header and determine whether the content is a gzip file.
+/// If [`gzbuffer`] is used, it should be called before `gzdirect`.
+///
+/// # Returns
+///
+/// 0 if `file` is null.
+///
+/// If `file` is being read,
+/// * 1 if the contents are being read directly, without decompression.
+/// * 0 if the contents are being decompressed when read.
+///
+/// If `file` is being written,
+/// * 1 if transparent mode was requested upon open (with the `"wT"` mode flag for [`gzopen`]).
+/// * 0 otherwise.
+///
+/// # Arguments
+///
+/// * `file` - A gzip file handle, or null
+///
+/// # Safety
+///
+/// `file`, if non-null, must be an open file handle obtained from [`gzopen`] or [`gzdopen`].
+#[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzdirect))]
+pub unsafe extern "C-unwind" fn gzdirect(file: gzFile) -> c_int {
+    let Some(state) = (unsafe { file.cast::<GzState>().as_mut() }) else {
+        return 0;
+    };
+
+    // In write mode, the direct flag was set in `gzopen_help`. In read mode, the
+    // direct status is determined lazily on the first read operation. If the first
+    // read hasn't happened yet, we can look at the header now to determine if the
+    // file is in gzip format.
+    if state.mode == GzMode::GZ_READ && state.how == How::Look && state.have == 0 {
+        _ = unsafe { gz_look(state) };
+    }
+
+    state.direct as _
+}
+
+// Given a gzip file opened for reading, check for a gzip header, and set
+// `state.direct` accordingly.
+//
+// # Returns
+//
+// * `Ok` on success
+// * `Err` on failure
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_look(state: &mut GzState) -> Result<(), ()> {
+    // Allocate buffers if needed.
+    if state.input.is_null() {
+        state.in_size = state.want;
+        let Some(input) = ALLOCATOR.allocate_slice_raw::<u8>(state.in_size) else {
+            // Safety:
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.input = input.as_ptr();
+    }
+    if state.output.is_null() {
+        state.out_size = state.want * 2;
+        let Some(output) = ALLOCATOR.allocate_slice_raw::<u8>(state.out_size) else {
+            // Safety: The caller confirmed the validity of state, and free_buffers checks
+            // for null input and output pointers internally.
+            unsafe { free_buffers(state) };
+            // Safety: The caller confirmed the validity of state.
+            unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+            return Err(());
+        };
+        state.output = output.as_ptr();
+    }
+
+    // Allocate memory for inflate.
+    state.stream.avail_in = 0;
+    state.stream.next_in = ptr::null_mut();
+    // Safety: `gzopen_help` initialized `state.stream`'s `zalloc`, `zfree`, and
+    // `opaque` fields as needed by `inflateInit2`.
+    if unsafe { inflateInit2(&mut state.stream as *mut z_stream, MAX_WBITS + 16) } != Z_OK {
+        // Safety: The caller confirmed the validity of `state`, and `free_buffers` checks
+        // for null input and output pointers internally.
+        unsafe { free_buffers(state) };
+        // Safety: The caller confirmed the validity of `state`.
+        unsafe { gz_error(state, Some((Z_MEM_ERROR, "out of memory"))) };
+        return Err(());
+    }
+
+    // Get at least the magic bytes in the input buffer.
+    if state.stream.avail_in < 2 {
+        // Safety: The caller confirmed the validity of `state`.
+        let Ok(_) = (unsafe { gz_avail(state) }) else {
+            return Err(());
+        };
+        if state.stream.avail_in == 0 {
+            return Ok(());
+        }
+    }
+
+    // Look for gzip magic bytes.
+    // Safety: `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    if state.stream.avail_in > 1
+        && unsafe { *state.stream.next_in } == 31
+        && unsafe { *state.stream.next_in.add(1) } == 139
+    {
+        // Safety: We initialized `state.stream` with `inflateInit2` above.
+        unsafe { inflateReset(&mut state.stream as *mut z_stream) };
+        state.how = How::Gzip;
+        state.direct = false;
+        return Ok(());
+    }
+
+    // No gzip header. If we were decoding gzip before, the remaining bytes
+    // are trailing garbage that can be ignored.
+    // FIXME: Add test coverage for this case (which may require the remaining decompression logic).
+    if !state.direct {
+        state.stream.avail_in = 0;
+        state.eof = true;
+        state.have = 0;
+        return Ok(());
+    }
+
+    // The file is not in gzip format, so enable direct mode, and copy all
+    // buffered input to the output.
+    // Safety:
+    // * `state.output` was allocated above.
+    // * `gz_avail` ensures that `next_in` points to at least `avail_in` readable bytes.
+    unsafe {
+        ptr::copy(
+            state.stream.next_in,
+            state.output,
+            state.stream.avail_in as usize,
+        )
+    };
+    state.next = state.output;
+    state.have = state.stream.avail_in;
+    state.stream.avail_in = 0;
+    state.how = How::Copy;
+    state.direct = true;
+
+    Ok(())
+}
+
+// Load data into the input buffer and set the eof flag if the last of the data has been
+// loaded.
+//
+// # Returns
+//
+// * `Ok(n)` on success, where `n` is the number of bytes available (`state.stream.avail_in`)
+// * `Err` on error
+//
+// # Safety
+//
+// `state` must have been properly initialized, e.g. by [`gzopen_help`].
+unsafe fn gz_avail(state: &mut GzState) -> Result<usize, ()> {
+    if state.err != Z_OK && state.err != Z_BUF_ERROR {
+        return Err(());
+    }
+    if !state.eof {
+        if state.stream.avail_in != 0 {
+            // Copy any remaining input to the start.
+            unsafe {
+                ptr::copy(
path: libz-rs-sys/src/gz.rs
user: login: folkertdev
body: Maybe add a note then that they may overlap
created_at: 2025-04-16T19:45:51Z
author_association: MEMBER

PR comments:
user: login: brian-pane
created_at: 2025-04-14T16:09:27Z
body: Thanks for reviewing, I'll post an update later today. Do you have a recommended test coverage measurement tool? I'm looking for something I can easily run locally.
author_association: CONTRIBUTOR
user: login: folkertdev
created_at: 2025-04-14T16:15:06Z
body: Yes, we use `cargo llvm-cov test --open` (or `--html` to just update the html and not open a (new) browser tab). see https://crates.io/crates/cargo-llvm-cov/0.1.13#installation for how to install that.
author_association: MEMBER
user: login: brian-pane
created_at: 2025-04-14T16:52:41Z
body: Is it considered okay to do file I/O from unit tests within `libz-rs-sys/src/gz.rs`, and to add a directory `libz-rs-sys/src/test-data`, similar to what's in `test-libz-rs-sys`? (Some of the branches I want to cover with unit tests are difficult to trigger from outside the crate.)
author_association: CONTRIBUTOR
user: login: folkertdev
created_at: 2025-04-14T17:24:15Z
body: Let's go with that for now, and then see if there is a more elegant way. It might be possible to e.g. fake a file descriptor in some cases, or expose some testing functions with an additional feature flag to `test-libz-rs-sys`.
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-14T22:24:59Z
body: ## [Codecov](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/341?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 `89.65517%` with `30 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/341?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/341?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) | 89.65% | [30 Missing :warning: ](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/341?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/341/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | Coverage Δ | |
|---|---|---|
| [test-aarch64-apple-darwin](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/341/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `89.45% <51.72%> (?)` | |
| [test-x86_64-apple-darwin](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/341/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `87.62% <51.72%> (?)` | |
| [test-x86_64-unknown-linux-gnu](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/341/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=trifectatechfoundation) | `90.07% <89.65%> (?)` | |

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/341?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/341?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) | `94.88% <89.65%> (+7.95%)` | :arrow_up: |

... and [39 files with indirect coverage changes](https://app.codecov.io/gh/trifectatechfoundation/zlib-rs/pull/341/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-14T22:46:43Z
body: To make my new test cases work under Miri when running locally, I had to use `MIRIFLAGS="-Zmiri-disable-isolation"`. Is there an equivalent of that setting for the CI run?
author_association: CONTRIBUTOR
user: login: brian-pane
created_at: 2025-04-15T16:43:30Z
body: Oops, I accidentally removed the Miri isolation fix. I'm restoring it now...
author_association: CONTRIBUTOR