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/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 \ 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 6dd56970c..cfc355a6e 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; @@ -1163,6 +1168,29 @@ extern "C" { ) -> CURLMcode; } +#[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 737bc7c41..83cdce978 100644 --- a/src/easy/handle.rs +++ b/src/easy/handle.rs @@ -12,6 +12,8 @@ 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}; use crate::Error; @@ -1470,6 +1472,12 @@ impl Easy { pub fn take_error_buf(&self) -> Option { 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() + } } impl EasyData { diff --git a/src/easy/handler.rs b/src/easy/handler.rs index e944b877c..61b3b5bc8 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -15,6 +15,8 @@ use socket2::Socket; use crate::easy::form; use crate::easy::list; +#[cfg(feature = "mime")] +use crate::easy::mime::{Mime, MimeHandle}; use crate::easy::windows; use crate::easy::{Form, List}; use crate::panic; @@ -388,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 {} @@ -598,6 +603,8 @@ impl Easy2 { form: None, error_buf: RefCell::new(vec![0; curl_sys::CURL_ERROR_SIZE]), handler, + #[cfg(feature = "mime")] + mime: Default::default(), }), }; ret.default_configure(); @@ -3511,6 +3518,27 @@ impl Easy2 { } } +#[cfg(feature = "mime")] +impl Easy2 { + /// Create a mime handle attached to this [Easy2] instance. + pub fn add_mime(&mut self) -> Mime { + Mime::new(self) + } + + 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); + } + + self.cvt(rc) + } +} + impl fmt::Debug for Easy2 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Easy") diff --git a/src/easy/mime.rs b/src/easy/mime.rs new file mode 100644 index 000000000..c1f49a484 --- /dev/null +++ b/src/easy/mime.rs @@ -0,0 +1,147 @@ +use crate::easy::Easy2; +use crate::error::Error; +use curl_sys::{ + 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; + +#[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> { + pub(super) handle: MimeHandle, + easy: &'e mut Easy2, +} + +impl<'a, T> Mime<'a, T> { + /// Create a mime handle + 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(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 + pub fn add_part(&mut self) -> MimePart<'a> { + MimePart::new(self) + } +} + +#[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.0) }; + 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)) + } +} + +#[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(); + } +} diff --git a/src/easy/mod.rs b/src/easy/mod.rs index 0b0e23a7e..d1af54302 100644 --- a/src/easy/mod.rs +++ b/src/easy/mod.rs @@ -11,6 +11,8 @@ mod form; mod handle; mod handler; mod list; +#[cfg(feature = "mime")] +mod mime; mod windows; pub use self::form::{Form, Part};