From 2ed79cbe41e8562e7fb45027486a4bae077d768b Mon Sep 17 00:00:00 2001 From: Noam Meltzer Date: Thu, 23 Nov 2023 20:12:30 +0200 Subject: [PATCH 1/4] add mime support --- curl-sys/lib.rs | 18 +++++++ src/easy/handle.rs | 6 +++ src/easy/handler.rs | 21 +++++++++ src/easy/mime.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++ src/easy/mod.rs | 1 + 5 files changed, 158 insertions(+) create mode 100644 src/easy/mime.rs diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index 6dd56970c..e5edeb749 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -65,6 +65,9 @@ pub enum curl_httppost { // pub userp: *mut c_void, } +pub enum curl_mime {} +pub enum curl_mimepart {} + // pub const HTTPPOST_FILENAME: c_long = 1 << 0; // pub const HTTPPOST_READFILE: c_long = 1 << 1; // pub const HTTPPOST_PTRNAME: c_long = 1 << 2; @@ -607,6 +610,8 @@ pub const CURLOPT_PROXY_SSL_OPTIONS: CURLoption = CURLOPTTYPE_LONG + 261; pub const CURLOPT_ABSTRACT_UNIX_SOCKET: CURLoption = CURLOPTTYPE_OBJECTPOINT + 264; +pub const CURLOPT_MIMEPOST: CURLoption = CURLOPTTYPE_OBJECTPOINT + 269; + pub const CURLOPT_DOH_URL: CURLoption = CURLOPTTYPE_OBJECTPOINT + 279; pub const CURLOPT_UPLOAD_BUFFERSIZE: CURLoption = CURLOPTTYPE_LONG + 280; @@ -1161,6 +1166,19 @@ extern "C" { sockfd: curl_socket_t, sockp: *mut c_void, ) -> CURLMcode; + + pub fn curl_mime_init(easy_handle: *mut CURL) -> *mut curl_mime; + pub fn curl_mime_free(mime_handle: *mut curl_mime); + pub fn curl_mime_addpart(mime_handle: *mut curl_mime) -> *mut curl_mimepart; + pub fn curl_mime_data( + mimepart: *mut curl_mimepart, + data: *const c_char, + datasize: size_t, + ) -> CURLcode; + pub fn curl_mime_name(part: *mut curl_mimepart, name: *const c_char) -> CURLcode; + pub fn curl_mime_filename(part: *mut curl_mimepart, filename: *const c_char) -> CURLcode; + pub fn curl_mime_type(part: *mut curl_mimepart, mimetype: *const c_char) -> CURLcode; + pub fn curl_mime_subparts(part: *mut curl_mimepart, subparts: *mut curl_mime) -> CURLcode; } pub fn rust_crate_version() -> &'static str { diff --git a/src/easy/handle.rs b/src/easy/handle.rs index 737bc7c41..1549dc457 100644 --- a/src/easy/handle.rs +++ b/src/easy/handle.rs @@ -12,6 +12,7 @@ use libc::c_void; use crate::easy::handler::{self, InfoType, ReadError, SeekResult, WriteError}; use crate::easy::handler::{Auth, NetRc, PostRedirections, ProxyType, SslOpt}; use crate::easy::handler::{HttpVersion, IpResolve, SslVersion, TimeCondition}; +use crate::easy::mime::Mime; use crate::easy::{Easy2, Handler}; use crate::easy::{Form, List}; use crate::Error; @@ -1470,6 +1471,11 @@ impl Easy { pub fn take_error_buf(&self) -> Option { self.inner.take_error_buf() } + + /// Same as [`Easy2::add_mime`](struct.Easy2.html#method.add_mime) + pub fn add_mime(&mut self) -> Mime { + self.inner.add_mime() + } } impl EasyData { diff --git a/src/easy/handler.rs b/src/easy/handler.rs index e944b877c..b26d93d60 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -15,6 +15,7 @@ use socket2::Socket; use crate::easy::form; use crate::easy::list; +use crate::easy::mime::Mime; use crate::easy::windows; use crate::easy::{Form, List}; use crate::panic; @@ -378,6 +379,8 @@ pub fn ssl_ctx(cx: *mut c_void) -> Result<(), Error> { /// ``` pub struct Easy2 { inner: Box>, + /// Mime handles to free upon drop + mimes: Vec<*mut curl_sys::curl_mime>, } struct Inner { @@ -599,6 +602,7 @@ impl Easy2 { error_buf: RefCell::new(vec![0; curl_sys::CURL_ERROR_SIZE]), handler, }), + mimes: vec![], }; ret.default_configure(); ret @@ -3509,6 +3513,20 @@ impl Easy2 { } Err(err) } + + /// Create a mime handle attached to this [Easy2] instance. + pub fn add_mime(&mut self) -> Mime { + Mime::new(self) + } + + pub(crate) fn mimepost(&mut self, mime_handle: *mut curl_sys::curl_mime) -> Result<(), Error> { + self.mimes.push(mime_handle); + + let rc = unsafe { + curl_sys::curl_easy_setopt(self.raw(), curl_sys::CURLOPT_MIMEPOST, mime_handle) + }; + self.cvt(rc) + } } impl fmt::Debug for Easy2 { @@ -3524,6 +3542,9 @@ impl Drop for Easy2 { fn drop(&mut self) { unsafe { curl_sys::curl_easy_cleanup(self.inner.handle); + for &mime_handle in self.mimes.iter() { + curl_sys::curl_mime_free(mime_handle); + } } } } diff --git a/src/easy/mime.rs b/src/easy/mime.rs new file mode 100644 index 000000000..b038977f2 --- /dev/null +++ b/src/easy/mime.rs @@ -0,0 +1,112 @@ +use crate::easy::Easy2; +use crate::error::Error; +use curl_sys::{ + curl_mime_addpart, curl_mime_data, curl_mime_filename, curl_mime_free, curl_mime_init, + curl_mime_name, curl_mime_type, curl_mimepart, CURLcode, CURLE_OK, +}; +use std::ffi::CString; +use std::marker::PhantomData; +use std::ptr::null_mut; + +#[derive(Debug)] +pub struct Mime<'e, E> { + handle: *mut curl_sys::curl_mime, + easy: &'e mut Easy2, +} + +impl<'a, T> Mime<'a, T> { + /// Create a mime handle + pub(crate) fn new(easy: &'a mut Easy2) -> Self { + let handle = unsafe { curl_mime_init(easy.raw()) }; + assert!(!handle.is_null()); + + Self { handle, easy } + } + + /// Finalize creation of a mime post. + pub fn post(mut self) -> Result<(), Error> { + // once giving the mime handle to `Easy2` it is now their responsibility to free the handle. + // so we need to make sure `Drop` below won't try to free it. + let mime_handle = self.handle; + self.handle = null_mut(); + self.easy.mimepost(mime_handle) + } + + /// Append a new empty part to a mime structure + pub fn add_part(&mut self) -> MimePart<'a> { + MimePart::new(self) + } +} + +impl Drop for Mime<'_, E> { + fn drop(&mut self) { + // we only need to free mime handles which hadn't been given to the ownership of `Easy2`. + if !self.handle.is_null() { + unsafe { curl_mime_free(self.handle) } + } + } +} + +#[derive(Debug)] +pub struct MimePart<'a> { + handle: *mut curl_mimepart, + // attach to the lifetime of our [Mime] handle, but without taking ownership + _lifetime: PhantomData<&'a ()>, +} + +impl<'a> MimePart<'a> { + fn new(mime: &mut Mime) -> Self { + let handle = unsafe { curl_mime_addpart(mime.handle) }; + assert!(!handle.is_null()); + + Self { + handle, + _lifetime: Default::default(), + } + } + + /// Set a mime part's body data + pub fn set_data(self, data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + let code = unsafe { curl_mime_data(self.handle, data.as_ptr() as *const _, data.len()) }; + code_ok(code).map(|_| self) + } + + /// Set a mime part's name + /// + /// # Panics + /// If `name` contains nul bytes, panic will occur. + pub fn set_name(self, name: &str) -> Result { + let data = CString::new(name).unwrap(); + let code = unsafe { curl_mime_name(self.handle, data.as_ptr()) }; + code_ok(code).map(|_| self) + } + + /// Set a mime part's remote file name + /// + /// # Panics + /// If `filename` contains nul bytes, panic will occur. + pub fn set_filename(self, filename: &str) -> Result { + let data = CString::new(filename).unwrap(); + let code = unsafe { curl_mime_filename(self.handle, data.as_ptr()) }; + code_ok(code).map(|_| self) + } + + /// Set a mime part's content type + /// + /// # Panics + /// If `content_type` contains nul bytes, panic will occur. + pub fn set_content_type(self, content_type: &str) -> Result { + let data = CString::new(content_type).unwrap(); + let code = unsafe { curl_mime_type(self.handle, data.as_ptr()) }; + code_ok(code).map(|_| self) + } +} + +fn code_ok(code: CURLcode) -> Result<(), Error> { + if code == CURLE_OK { + Ok(()) + } else { + Err(Error::new(code)) + } +} diff --git a/src/easy/mod.rs b/src/easy/mod.rs index 0b0e23a7e..c9855e093 100644 --- a/src/easy/mod.rs +++ b/src/easy/mod.rs @@ -11,6 +11,7 @@ mod form; mod handle; mod handler; mod list; +mod mime; mod windows; pub use self::form::{Form, Part}; From 2447d15a0e460e275d9b01f136f46814dd0a4a31 Mon Sep 17 00:00:00 2001 From: Noam Meltzer Date: Sun, 26 Nov 2023 16:33:37 +0200 Subject: [PATCH 2/4] CI: switch linux64-curl docker to ubuntu:22.04 ubuntu:16.04 version of libcurl doesn't have the mime feature compiled in --- ci/Dockerfile-linux64-curl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/Dockerfile-linux64-curl b/ci/Dockerfile-linux64-curl index dd58df136..8a7864343 100644 --- a/ci/Dockerfile-linux64-curl +++ b/ci/Dockerfile-linux64-curl @@ -1,4 +1,4 @@ -FROM ubuntu:16.04 +FROM ubuntu:18.04 RUN apt-get update RUN apt-get install -y --no-install-recommends \ From e37a551c6fdf84c7e98d3b07cc00650c3d9a5718 Mon Sep 17 00:00:00 2001 From: Noam Meltzer Date: Mon, 27 Nov 2023 09:43:09 +0200 Subject: [PATCH 3/4] move mime behind an opt-in feature --- Cargo.toml | 1 + curl-sys/Cargo.toml | 1 + curl-sys/lib.rs | 34 ++++++++++++++++++++++------------ src/easy/handle.rs | 2 ++ src/easy/handler.rs | 8 ++++++++ src/easy/mod.rs | 1 + 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e7033299..866ff3ad4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ zlib-ng-compat = ["curl-sys/zlib-ng-compat", "static-curl"] upkeep_7_62_0 = ["curl-sys/upkeep_7_62_0"] poll_7_68_0 = ["curl-sys/poll_7_68_0"] ntlm = ["curl-sys/ntlm"] +mime = ["curl-sys/mime"] [[test]] name = "atexit" diff --git a/curl-sys/Cargo.toml b/curl-sys/Cargo.toml index 79e0f3b69..97aeaa54c 100644 --- a/curl-sys/Cargo.toml +++ b/curl-sys/Cargo.toml @@ -58,3 +58,4 @@ zlib-ng-compat = ["libz-sys/zlib-ng", "static-curl"] upkeep_7_62_0 = [] poll_7_68_0 = [] ntlm = [] +mime = [] \ No newline at end of file diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index e5edeb749..cfc355a6e 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -1166,21 +1166,31 @@ extern "C" { sockfd: curl_socket_t, sockp: *mut c_void, ) -> CURLMcode; +} - pub fn curl_mime_init(easy_handle: *mut CURL) -> *mut curl_mime; - pub fn curl_mime_free(mime_handle: *mut curl_mime); - pub fn curl_mime_addpart(mime_handle: *mut curl_mime) -> *mut curl_mimepart; - pub fn curl_mime_data( - mimepart: *mut curl_mimepart, - data: *const c_char, - datasize: size_t, - ) -> CURLcode; - pub fn curl_mime_name(part: *mut curl_mimepart, name: *const c_char) -> CURLcode; - pub fn curl_mime_filename(part: *mut curl_mimepart, filename: *const c_char) -> CURLcode; - pub fn curl_mime_type(part: *mut curl_mimepart, mimetype: *const c_char) -> CURLcode; - pub fn curl_mime_subparts(part: *mut curl_mimepart, subparts: *mut curl_mime) -> CURLcode; +#[cfg(feature = "mime")] +mod mime { + use super::*; + + extern "C" { + pub fn curl_mime_init(easy_handle: *mut CURL) -> *mut curl_mime; + pub fn curl_mime_free(mime_handle: *mut curl_mime); + pub fn curl_mime_addpart(mime_handle: *mut curl_mime) -> *mut curl_mimepart; + pub fn curl_mime_data( + mimepart: *mut curl_mimepart, + data: *const c_char, + datasize: size_t, + ) -> CURLcode; + pub fn curl_mime_name(part: *mut curl_mimepart, name: *const c_char) -> CURLcode; + pub fn curl_mime_filename(part: *mut curl_mimepart, filename: *const c_char) -> CURLcode; + pub fn curl_mime_type(part: *mut curl_mimepart, mimetype: *const c_char) -> CURLcode; + pub fn curl_mime_subparts(part: *mut curl_mimepart, subparts: *mut curl_mime) -> CURLcode; + } } +#[cfg(feature = "mime")] +pub use mime::*; + pub fn rust_crate_version() -> &'static str { env!("CARGO_PKG_VERSION") } diff --git a/src/easy/handle.rs b/src/easy/handle.rs index 1549dc457..83cdce978 100644 --- a/src/easy/handle.rs +++ b/src/easy/handle.rs @@ -12,6 +12,7 @@ use libc::c_void; use crate::easy::handler::{self, InfoType, ReadError, SeekResult, WriteError}; use crate::easy::handler::{Auth, NetRc, PostRedirections, ProxyType, SslOpt}; use crate::easy::handler::{HttpVersion, IpResolve, SslVersion, TimeCondition}; +#[cfg(feature = "mime")] use crate::easy::mime::Mime; use crate::easy::{Easy2, Handler}; use crate::easy::{Form, List}; @@ -1472,6 +1473,7 @@ impl Easy { self.inner.take_error_buf() } + #[cfg(feature = "mime")] /// Same as [`Easy2::add_mime`](struct.Easy2.html#method.add_mime) pub fn add_mime(&mut self) -> Mime { self.inner.add_mime() diff --git a/src/easy/handler.rs b/src/easy/handler.rs index b26d93d60..3a5517ba9 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -15,6 +15,7 @@ use socket2::Socket; use crate::easy::form; use crate::easy::list; +#[cfg(feature = "mime")] use crate::easy::mime::Mime; use crate::easy::windows; use crate::easy::{Form, List}; @@ -379,6 +380,7 @@ pub fn ssl_ctx(cx: *mut c_void) -> Result<(), Error> { /// ``` pub struct Easy2 { inner: Box>, + #[cfg(feature = "mime")] /// Mime handles to free upon drop mimes: Vec<*mut curl_sys::curl_mime>, } @@ -602,6 +604,7 @@ impl Easy2 { error_buf: RefCell::new(vec![0; curl_sys::CURL_ERROR_SIZE]), handler, }), + #[cfg(feature = "mime")] mimes: vec![], }; ret.default_configure(); @@ -3513,7 +3516,10 @@ impl Easy2 { } Err(err) } +} +#[cfg(feature = "mime")] +impl Easy2 { /// Create a mime handle attached to this [Easy2] instance. pub fn add_mime(&mut self) -> Mime { Mime::new(self) @@ -3542,6 +3548,8 @@ impl Drop for Easy2 { fn drop(&mut self) { unsafe { curl_sys::curl_easy_cleanup(self.inner.handle); + + #[cfg(feature = "mime")] for &mime_handle in self.mimes.iter() { curl_sys::curl_mime_free(mime_handle); } diff --git a/src/easy/mod.rs b/src/easy/mod.rs index c9855e093..d1af54302 100644 --- a/src/easy/mod.rs +++ b/src/easy/mod.rs @@ -11,6 +11,7 @@ mod form; mod handle; mod handler; mod list; +#[cfg(feature = "mime")] mod mime; mod windows; From 2994ec75de9fc7e5fb13b6b3ab4f51e8e6b0f1de Mon Sep 17 00:00:00 2001 From: Noam Meltzer Date: Mon, 27 Nov 2023 10:26:16 +0200 Subject: [PATCH 4/4] clean up relationship between Mime & Easy2 objects - The raw mime handle is now wrapped by a rust struct which will call curl_mime_free() upon Drop - Easy2 now keeps the MimeHandle as an Option instead of Vec - The above Option is kept inside Inner instead of directly under Easy2 --- src/easy/handler.rs | 31 +++++++++-------- src/easy/mime.rs | 81 ++++++++++++++++++++++++++++++++------------- 2 files changed, 73 insertions(+), 39 deletions(-) diff --git a/src/easy/handler.rs b/src/easy/handler.rs index 3a5517ba9..61b3b5bc8 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -16,7 +16,7 @@ use socket2::Socket; use crate::easy::form; use crate::easy::list; #[cfg(feature = "mime")] -use crate::easy::mime::Mime; +use crate::easy::mime::{Mime, MimeHandle}; use crate::easy::windows; use crate::easy::{Form, List}; use crate::panic; @@ -380,9 +380,6 @@ pub fn ssl_ctx(cx: *mut c_void) -> Result<(), Error> { /// ``` pub struct Easy2 { inner: Box>, - #[cfg(feature = "mime")] - /// Mime handles to free upon drop - mimes: Vec<*mut curl_sys::curl_mime>, } struct Inner { @@ -393,6 +390,9 @@ struct Inner { form: Option
, error_buf: RefCell>, handler: H, + #[cfg(feature = "mime")] + /// [MimeHandle] object to drop when it's safe + mime: Option, } unsafe impl Send for Inner {} @@ -603,9 +603,9 @@ impl Easy2 { form: None, error_buf: RefCell::new(vec![0; curl_sys::CURL_ERROR_SIZE]), handler, + #[cfg(feature = "mime")] + mime: Default::default(), }), - #[cfg(feature = "mime")] - mimes: vec![], }; ret.default_configure(); ret @@ -3525,12 +3525,16 @@ impl Easy2 { Mime::new(self) } - pub(crate) fn mimepost(&mut self, mime_handle: *mut curl_sys::curl_mime) -> Result<(), Error> { - self.mimes.push(mime_handle); + pub(super) fn mimepost(&mut self, mime: MimeHandle) -> Result<(), Error> { + assert!(self.inner.mime.is_none()); + + let rc = + unsafe { curl_sys::curl_easy_setopt(self.raw(), curl_sys::CURLOPT_MIMEPOST, mime.0) }; + + if rc == curl_sys::CURLE_OK { + self.inner.mime = Some(mime); + } - let rc = unsafe { - curl_sys::curl_easy_setopt(self.raw(), curl_sys::CURLOPT_MIMEPOST, mime_handle) - }; self.cvt(rc) } } @@ -3548,11 +3552,6 @@ impl Drop for Easy2 { fn drop(&mut self) { unsafe { curl_sys::curl_easy_cleanup(self.inner.handle); - - #[cfg(feature = "mime")] - for &mime_handle in self.mimes.iter() { - curl_sys::curl_mime_free(mime_handle); - } } } } diff --git a/src/easy/mime.rs b/src/easy/mime.rs index b038977f2..c1f49a484 100644 --- a/src/easy/mime.rs +++ b/src/easy/mime.rs @@ -1,35 +1,49 @@ use crate::easy::Easy2; use crate::error::Error; use curl_sys::{ - curl_mime_addpart, curl_mime_data, curl_mime_filename, curl_mime_free, curl_mime_init, - curl_mime_name, curl_mime_type, curl_mimepart, CURLcode, CURLE_OK, + curl_mime, curl_mime_addpart, curl_mime_data, curl_mime_filename, curl_mime_free, + curl_mime_init, curl_mime_name, curl_mime_type, curl_mimepart, CURLcode, CURL, CURLE_OK, }; use std::ffi::CString; use std::marker::PhantomData; -use std::ptr::null_mut; + +#[derive(Debug)] +pub(super) struct MimeHandle(pub *mut curl_mime); + +impl MimeHandle { + fn new(easy: *mut CURL) -> Self { + let handle = unsafe { curl_mime_init(easy) }; + assert!(!handle.is_null()); + + Self(handle) + } +} + +impl Drop for MimeHandle { + fn drop(&mut self) { + unsafe { curl_mime_free(self.0) } + } +} #[derive(Debug)] pub struct Mime<'e, E> { - handle: *mut curl_sys::curl_mime, + pub(super) handle: MimeHandle, easy: &'e mut Easy2, } impl<'a, T> Mime<'a, T> { /// Create a mime handle - pub(crate) fn new(easy: &'a mut Easy2) -> Self { - let handle = unsafe { curl_mime_init(easy.raw()) }; - assert!(!handle.is_null()); + pub(super) fn new(easy: &'a mut Easy2) -> Self { + let handle = MimeHandle::new(easy.raw()); Self { handle, easy } } /// Finalize creation of a mime post. - pub fn post(mut self) -> Result<(), Error> { - // once giving the mime handle to `Easy2` it is now their responsibility to free the handle. - // so we need to make sure `Drop` below won't try to free it. - let mime_handle = self.handle; - self.handle = null_mut(); - self.easy.mimepost(mime_handle) + pub fn post(self) -> Result<(), Error> { + // We give ownership on `MimeHandle` to `Easy2`. `Easy2` will keep record of this object + // until it is safe to free (drop) it. + self.easy.mimepost(self.handle) } /// Append a new empty part to a mime structure @@ -38,15 +52,6 @@ impl<'a, T> Mime<'a, T> { } } -impl Drop for Mime<'_, E> { - fn drop(&mut self) { - // we only need to free mime handles which hadn't been given to the ownership of `Easy2`. - if !self.handle.is_null() { - unsafe { curl_mime_free(self.handle) } - } - } -} - #[derive(Debug)] pub struct MimePart<'a> { handle: *mut curl_mimepart, @@ -56,7 +61,7 @@ pub struct MimePart<'a> { impl<'a> MimePart<'a> { fn new(mime: &mut Mime) -> Self { - let handle = unsafe { curl_mime_addpart(mime.handle) }; + let handle = unsafe { curl_mime_addpart(mime.handle.0) }; assert!(!handle.is_null()); Self { @@ -110,3 +115,33 @@ fn code_ok(code: CURLcode) -> Result<(), Error> { Err(Error::new(code)) } } + +#[cfg(test)] +mod tests { + use crate::easy::Easy; + + /// Trivial test which checks that objects can be used as planned. + #[test] + fn test_ownership() { + let mut easy = Easy::new(); + let mut mime = easy.add_mime(); + + for i in 1..5 { + let name = format!("name{i}"); + let data = format!("data{i}"); + let fname = format!("fname{i}"); + + mime.add_part() + .set_data(name) + .unwrap() + .set_data(data) + .unwrap() + .set_filename(&fname) + .unwrap() + .set_content_type("plain/text") + .unwrap(); + } + + mime.post().unwrap(); + } +}