-
-
Notifications
You must be signed in to change notification settings - Fork 29
Partial implementation of gzopen and gzclose #326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e1cc9ac
Partial implementation of gzopen and gzclose
brianpane 8dd309b
Code cleanups
brianpane 662dac9
gz.rs libc portability fixes
brianpane d07f7bd
Fix size_of import
brianpane e338b66
Remove gz support for O_CLOEXEC for now, for older Linux compatibility
brianpane 3a3323d
Remove use of libc::strdup that is incompatible with Rust allocator
brianpane 6de780d
Use Rust naming convention instead of C for internal gz state struct
brianpane ac75bf5
formatting
folkertdev ae76dd6
pass `--dir` to the wasmtime runner on CI
folkertdev d52a3d3
test macro cleanup
folkertdev caa7fc0
only `pub(crate)` use the `prefix` macro
folkertdev 6ad5ffd
Code review cleanups
brianpane 3fd9dfa
Use the new Allocator API
brianpane 3d20506
Specify type for deallocate calls
brianpane 82da4f5
don't drop temporary `CString` too early
folkertdev e7320e3
don't run fuzzer clippy with the `gz` feature
folkertdev bde3c58
document the return value of `gzclose`
folkertdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| use zlib_rs::allocate::*; | ||
| pub use zlib_rs::c_api::*; | ||
|
|
||
| use core::ffi::{c_char, c_int, c_uint, CStr}; | ||
| use core::ptr; | ||
| use libc::{O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_TRUNC, O_WRONLY, SEEK_CUR, SEEK_END}; | ||
| use zlib_rs::deflate::Strategy; | ||
|
|
||
| /// In the zlib C API, this structure exposes just enough of the internal state | ||
| /// of an open gzFile to support the gzgetc() C macro. Since Rust code won't be | ||
| /// using that C macro, we define gzFile_s as an empty structure. The first fields | ||
| /// in GzState match what would be in the C version of gzFile_s. | ||
| pub enum gzFile_s {} | ||
|
|
||
| /// File handle for an open gzip file. | ||
| pub type gzFile = *mut gzFile_s; | ||
|
|
||
| // The internals of a gzip file handle (the thing gzFile actually points to, with the | ||
| // public gzFile_s part at the front for ABI compatibility). | ||
| #[repr(C)] | ||
| struct GzState { | ||
| // Public interface: | ||
| // These first three fields must match the structure gzFile_s in the C version | ||
| // of zlib. In the C library, a macro called gzgetc() reads and writes these | ||
| // fields directly. | ||
| have: c_uint, // number of bytes available at next | ||
| next: *const Bytef, // next byte of uncompressed data | ||
| pos: u64, // current offset in uncompressed data stream | ||
|
|
||
| // End of public interface: | ||
| // All fields after this point are opaque to C code using this library, | ||
| // so they can be rearranged without breaking compatibility. | ||
|
|
||
| // Fields used for both reading and writing | ||
| mode: GzMode, | ||
| fd: c_int, // file descriptor | ||
| path: *const c_char, | ||
| size: usize, // buffer size; can be 0 if not yet allocated | ||
| want: usize, // requested buffer size | ||
| input: *mut Bytef, // input buffer | ||
| output: *mut Bytef, // output buffer | ||
| direct: bool, // true in pass-through mode, false if processing gzip data | ||
|
|
||
| // Fields used just for reading | ||
| // FIXME: add the 'how' field when read support is implemented | ||
| start: i64, | ||
| eof: bool, // whether we have reached the end of the input file | ||
| past: bool, // whether a read past the end has been requested | ||
|
|
||
| // Fields used just for writing | ||
| level: i8, | ||
| strategy: Strategy, | ||
| reset: bool, // whether a reset is pending after a Z_FINISH | ||
|
|
||
| // Fields used for seek requests | ||
| skip: i64, // amount to skip (already rewound if backwards) | ||
| seek: bool, // whether a seek request is pending | ||
|
|
||
| // Error information | ||
| err: c_int, // last error (0 if no error) | ||
| msg: *const c_char, // error message from last error (NULL if none) | ||
|
|
||
| // FIXME: add the zstream field when read/write support is implemented | ||
bjorn3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Gzip operating modes | ||
| // NOTE: These values match what zlib-ng uses. | ||
| #[derive(PartialEq, Eq)] | ||
| enum GzMode { | ||
| GZ_NONE = 0, | ||
| GZ_READ = 7247, | ||
| GZ_WRITE = 31153, | ||
| GZ_APPEND = 1, | ||
| } | ||
|
|
||
| const GZBUFSIZE: usize = 128 * 1024; | ||
|
|
||
| #[cfg(feature = "rust-allocator")] | ||
| const ALLOCATOR: &Allocator = &Allocator::RUST; | ||
|
|
||
| #[cfg(not(feature = "rust-allocator"))] | ||
| #[cfg(feature = "c-allocator")] | ||
| const ALLOCATOR: &Allocator = &Allocator::C; | ||
|
|
||
| #[cfg(not(feature = "rust-allocator"))] | ||
| #[cfg(not(feature = "c-allocator"))] | ||
| compile_error!("Either rust-allocator or c-allocator feature is required"); | ||
|
|
||
| /// Open a gzip file for reading or writing. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// The caller must ensure that path and mode point to valid C strings. If the | ||
| /// return value is non-NULL, caller must delete it using only [`gzclose`]. | ||
| #[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzopen))] | ||
| pub unsafe extern "C-unwind" fn gzopen(path: *const c_char, mode: *const c_char) -> gzFile { | ||
| gzopen_help(path, -1, mode) | ||
| } | ||
|
|
||
| /// Internal implementation shared by gzopen and gzdopen. | ||
| /// | ||
| /// # Safety | ||
| /// The caller must ensure that path and mode are NULL or point to valid C strings. | ||
| unsafe fn gzopen_help(path: *const c_char, fd: c_int, mode: *const c_char) -> gzFile { | ||
| if path.is_null() || mode.is_null() { | ||
| return ptr::null_mut(); | ||
| } | ||
|
|
||
| let Some(state) = ALLOCATOR.allocate_zeroed_raw::<GzState>() else { | ||
| return ptr::null_mut(); | ||
| }; | ||
| let state = state.cast::<GzState>().as_mut(); | ||
| state.size = 0; | ||
| state.want = GZBUFSIZE; | ||
| state.msg = ptr::null(); | ||
|
|
||
| state.mode = GzMode::GZ_NONE; | ||
| state.level = crate::Z_DEFAULT_COMPRESSION as i8; | ||
| state.strategy = Strategy::Default; | ||
| state.direct = false; | ||
|
|
||
| let mut exclusive = false; | ||
| let mode = CStr::from_ptr(mode); | ||
| for &ch in mode.to_bytes() { | ||
| if ch.is_ascii_digit() { | ||
| state.level = (ch - b'0') as i8; | ||
| } else { | ||
| // FIXME implement the 'e' flag on platforms where O_CLOEXEC is supported | ||
folkertdev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| match ch { | ||
| b'r' => state.mode = GzMode::GZ_READ, | ||
| b'w' => state.mode = GzMode::GZ_WRITE, | ||
| b'a' => state.mode = GzMode::GZ_APPEND, | ||
| b'b' => {} // binary mode is the default | ||
| b'x' => exclusive = true, | ||
| b'f' => state.strategy = Strategy::Filtered, | ||
| b'h' => state.strategy = Strategy::HuffmanOnly, | ||
| b'R' => state.strategy = Strategy::Rle, | ||
| b'F' => state.strategy = Strategy::Fixed, | ||
| b'T' => state.direct = true, | ||
| _ => {} // for compatibility with zlib-ng, ignore unexpected characters in the mode | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Must specify read, write, or append | ||
| if state.mode == GzMode::GZ_NONE { | ||
| free_state(state); | ||
| return ptr::null_mut(); | ||
| } | ||
|
|
||
| // Can't force transparent read | ||
| if state.mode == GzMode::GZ_READ { | ||
| if state.direct { | ||
| free_state(state); | ||
| return ptr::null_mut(); | ||
| } | ||
| state.direct = true; | ||
| } | ||
|
|
||
| // Save the path name for error messages | ||
| // FIXME: support Windows wide characters for compatibility with zlib-ng | ||
| let len = libc::strlen(path) + 1; | ||
| let Some(path_copy) = ALLOCATOR.allocate_slice_raw::<c_char>(len) else { | ||
| free_state(state); | ||
| return ptr::null_mut(); | ||
| }; | ||
| let path_copy = path_copy.as_ptr().cast::<c_char>(); | ||
| libc::strncpy(path_copy, path, len); | ||
| state.path = path_copy; | ||
|
|
||
| // Open the file unless the caller passed a file descriptor. | ||
| if fd > -1 { | ||
| state.fd = fd; | ||
| } else { | ||
| let mut oflag: c_int = 0; | ||
| if state.mode == GzMode::GZ_READ { | ||
| oflag |= O_RDONLY; | ||
| } else { | ||
| oflag |= O_WRONLY | O_CREAT; | ||
| if exclusive { | ||
| oflag |= O_EXCL; | ||
| } | ||
| if state.mode == GzMode::GZ_WRITE { | ||
| oflag |= O_TRUNC; | ||
| } else { | ||
| oflag |= O_APPEND; | ||
| } | ||
| } | ||
| // FIXME: support _wopen for WIN32 | ||
| state.fd = libc::open(state.path, oflag, 0o666); | ||
| if state.fd == -1 { | ||
| free_state(state); | ||
| return ptr::null_mut(); | ||
| } | ||
| if state.mode == GzMode::GZ_APPEND { | ||
| libc::lseek(state.fd, 0, SEEK_END); | ||
| state.mode = GzMode::GZ_WRITE; | ||
| } | ||
| } | ||
|
|
||
| if state.mode == GzMode::GZ_READ { | ||
| // Save the current position for rewinding | ||
| state.start = libc::lseek(state.fd, 0, SEEK_CUR) as _; | ||
| if state.start == -1 { | ||
| state.start = 0; | ||
| } | ||
| } | ||
|
|
||
| // FIXME verify the file headers, and initialize the inflate/deflate state | ||
|
|
||
| // FIXME change this to core::ptr::from_mut(state).cast::<gzFile_s>() once MSRV >= 1.76 | ||
| (state as *mut GzState).cast::<gzFile_s>() | ||
| } | ||
|
|
||
| // Deallocate a GzState structure and all heap-allocated fields inside it. | ||
| // | ||
| // # Safety | ||
| // | ||
| // The caller must not use the state after passing it to this function. | ||
| unsafe fn free_state(state: &mut GzState) { | ||
| if !state.path.is_null() { | ||
| ALLOCATOR.deallocate::<c_char>(state.path.cast_mut(), libc::strlen(state.path) + 1); | ||
| } | ||
| ALLOCATOR.deallocate::<GzState>(state, 1); | ||
| } | ||
|
|
||
| /// Close an open gzip file and free the internal data structures referenced by the file handle. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// * [`Z_ERRNO`] if closing the file failed | ||
| /// * [`Z_OK`] otherwise | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// This function may be called at most once for any file handle. | ||
| #[cfg_attr(feature = "export-symbols", export_name = crate::prefix!(gzclose))] | ||
| pub unsafe extern "C-unwind" fn gzclose(file: gzFile) -> c_int { | ||
| let Some(state) = file.cast::<GzState>().as_mut() else { | ||
| return Z_STREAM_ERROR; | ||
| }; | ||
|
|
||
| // FIXME: once read/write support is added, clean up internal buffers | ||
|
|
||
| let err = libc::close(state.fd); | ||
| free_state(state); | ||
| if err == 0 { | ||
| Z_OK | ||
| } else { | ||
| Z_ERRNO | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| use zlib_rs::c_api::*; | ||
|
|
||
| use libz_rs_sys::gzclose; | ||
| use libz_rs_sys::gzopen; | ||
| use std::ffi::CString; | ||
| use std::ptr; | ||
|
|
||
| // Generate a file path relative to the project's root | ||
| macro_rules! path { | ||
| ($str:expr) => { | ||
| concat!(env!("CARGO_MANIFEST_DIR"), "/", $str) | ||
| }; | ||
| } | ||
|
|
||
| unsafe fn test_open(path: &str, mode: &str, should_succeed: bool) { | ||
| let cpath = CString::new(path).unwrap(); | ||
| let cmode = CString::new(mode).unwrap(); | ||
| let handle = gzopen(cpath.as_ptr(), cmode.as_ptr()); | ||
| assert_eq!(should_succeed, !handle.is_null(), "{path} {mode}"); | ||
| if !handle.is_null() { | ||
| assert_eq!(gzclose(handle), Z_OK); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn open_close() { | ||
| unsafe { | ||
| // Open a valid file for reading | ||
| test_open(path!("src/test-data/issue-109.gz"), "r", true); | ||
|
|
||
| // "b" for binary mode is optional | ||
| test_open(path!("src/test-data/issue-109.gz"), "rb", true); | ||
|
|
||
| // Mode must include r, w, or a | ||
| test_open(path!("src/test-data/issue-109.gz"), "", false); | ||
| test_open(path!("src/test-data/issue-109.gz"), "e", false); | ||
|
|
||
| // For zlib-ng compatibility, mode can't specify transparent read | ||
| test_open(path!("src/test-data/issue-109.gz"), "Tr", false); | ||
|
|
||
| // Read of a nonexistent file should fail | ||
| test_open(path!("src/test-data/no-such-file.gz"), "r", false); | ||
|
|
||
| // Closing a null file handle should return an error instead of crashing | ||
| assert_eq!(gzclose(ptr::null_mut()), Z_STREAM_ERROR); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.