Skip to content
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

add mime support (form api is deprecated according to libcurl) #537

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion ci/Dockerfile-linux64-curl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ubuntu:16.04
FROM ubuntu:18.04

RUN apt-get update
RUN apt-get install -y --no-install-recommends \
Expand Down
1 change: 1 addition & 0 deletions curl-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ zlib-ng-compat = ["libz-sys/zlib-ng", "static-curl"]
upkeep_7_62_0 = []
poll_7_68_0 = []
ntlm = []
mime = []
28 changes: 28 additions & 0 deletions curl-sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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")
}
Expand Down
8 changes: 8 additions & 0 deletions src/easy/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1470,6 +1472,12 @@ impl Easy {
pub fn take_error_buf(&self) -> Option<String> {
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<EasyData> {
self.inner.add_mime()
}
}

impl EasyData {
Expand Down
28 changes: 28 additions & 0 deletions src/easy/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -388,6 +390,9 @@ struct Inner<H> {
form: Option<Form>,
error_buf: RefCell<Vec<u8>>,
handler: H,
#[cfg(feature = "mime")]
/// [MimeHandle] object to drop when it's safe
mime: Option<MimeHandle>,
}

unsafe impl<H: Send> Send for Inner<H> {}
Expand Down Expand Up @@ -598,6 +603,8 @@ impl<H: Handler> Easy2<H> {
form: None,
error_buf: RefCell::new(vec![0; curl_sys::CURL_ERROR_SIZE]),
handler,
#[cfg(feature = "mime")]
mime: Default::default(),
}),
};
ret.default_configure();
Expand Down Expand Up @@ -3511,6 +3518,27 @@ impl<H> Easy2<H> {
}
}

#[cfg(feature = "mime")]
impl<H> Easy2<H> {
/// Create a mime handle attached to this [Easy2] instance.
pub fn add_mime(&mut self) -> Mime<H> {
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<H: fmt::Debug> fmt::Debug for Easy2<H> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Easy")
Expand Down
147 changes: 147 additions & 0 deletions src/easy/mime.rs
Original file line number Diff line number Diff line change
@@ -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<E>,
}

impl<'a, T> Mime<'a, T> {
/// Create a mime handle
pub(super) fn new(easy: &'a mut Easy2<T>) -> 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<E>(mime: &mut Mime<E>) -> 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<Self, Error> {
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<Self, Error> {
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<Self, Error> {
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<Self, Error> {
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();
}
}
2 changes: 2 additions & 0 deletions src/easy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ mod form;
mod handle;
mod handler;
mod list;
#[cfg(feature = "mime")]
mod mime;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mime is kinda its own API that isn't specific to the easy API, so I would make mime a top-level module.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see what you're saying about "kinda its own API".
However, mime API doesn't have a meaning without the easy API, quite similar to the form API. So basically, I've put the mime module in the same level as the form module.

I don't mind extracting it, just thought it is worth double checking this before doing so.

Your call though, I'll do what you ask :)

mod windows;

pub use self::form::{Form, Part};
Expand Down