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 support for CURLFORM_STREAM #164

Open
wants to merge 1 commit 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
58 changes: 57 additions & 1 deletion src/easy/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ use std::ffi::CString;
use std::fmt;
use std::path::Path;

use libc::c_void;

use FormError;
use curl_sys;
use easy::{list, List};
use easy::list;
use easy::{List, ReadError};

/// Multipart/formdata for an HTTP POST request.
///
Expand All @@ -16,6 +19,7 @@ pub struct Form {
headers: Vec<List>,
buffers: Vec<Vec<u8>>,
strings: Vec<CString>,
streams: Option<Box<StreamNode>>,
}

/// One part in a multipart upload, added to a `Form`.
Expand All @@ -26,10 +30,19 @@ pub struct Part<'form, 'data> {
error: Option<FormError>,
}

struct StreamNode {
stream: Box<FnMut(&mut [u8]) -> Result<usize, ReadError>>,
_next: Option<Box<StreamNode>>,
}

pub fn raw(form: &Form) -> *mut curl_sys::curl_httppost {
form.head
}

pub unsafe fn read(raw: *mut c_void, into: &mut [u8]) -> Result<usize, ReadError> {
((*(raw as *mut StreamNode)).stream)(into)
}

impl Form {
/// Creates a new blank form ready for the addition of new data.
pub fn new() -> Form {
Expand All @@ -39,6 +52,7 @@ impl Form {
headers: Vec::new(),
buffers: Vec::new(),
strings: Vec::new(),
streams: None,
}
}

Expand Down Expand Up @@ -254,6 +268,48 @@ impl<'form, 'data> Part<'form, 'data> {
self
}

/// Tells libcurl to use the callback provided to get data.
///
/// If you want the part to look like a file upload one, set the `filename`
/// parameter as well. Note that when using `stream` the `len` option must
/// also be set with the total expected length of the part unless the
/// formpost is sent chunked encoded in which case it can be `None`.
///
/// The callback provided must live for the `'static` lifetime and must
/// adhere to the `Send` bound.
pub fn stream<F>(&mut self,
len: Option<usize>,
f: F) -> &mut Self
where F: FnMut(&mut [u8]) -> Result<usize, ReadError> + 'static + Send,
{
self._stream(len, Box::new(f));
self
}

fn _stream(&mut self,
len: Option<usize>,
stream: Box<FnMut(&mut [u8]) -> Result<usize, ReadError> + Send>)
{
let mut node = Box::new(StreamNode {
stream: stream,
_next: self.form.streams.take(),
});
let pos = self.array.len() - 1;
let ptr = &mut *node as *mut StreamNode as usize;
assert!(ptr & 1 == 0);
self.array.insert(pos, curl_sys::curl_forms {
option: curl_sys::CURLFORM_STREAM,
value: (ptr | 1) as *mut _,
});
if let Some(len) = len {
self.array.insert(pos + 1, curl_sys::curl_forms {
option: curl_sys::CURLFORM_CONTENTSLENGTH,
value: len as *mut _,
});
}
self.form.streams = Some(node);
}

/// Attempts to add this part to the `Form` that it was created from.
///
/// If any error happens while adding, that error is returned, otherwise
Expand Down
14 changes: 13 additions & 1 deletion src/easy/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ impl<H: Handler> Easy2<H> {
let cb: curl_sys::curl_read_callback = read_cb::<H>;
self.setopt_ptr(curl_sys::CURLOPT_READFUNCTION, cb as *const _)
.expect("failed to set read callback");
assert!((ptr as usize) & 1 == 0);
self.setopt_ptr(curl_sys::CURLOPT_READDATA, ptr)
.expect("failed to set read callback");

Expand Down Expand Up @@ -2783,7 +2784,18 @@ extern fn read_cb<H: Handler>(ptr: *mut c_char,
panic::catch(|| unsafe {
let input = slice::from_raw_parts_mut(ptr as *mut u8,
size * nmemb);
match (*(data as *mut Inner<H>)).handler.read(input) {

// We use a bit of a "tricky encoding" here to handle CURLFORM_STREAM
// callbacks. If the lower bit of the data pointer is 1 it's a stream
// callback so we delegate to the `form` module, otherwise we handle
// that here as part of `Handler`.
let ret = if data as usize & 1 == 0 {
(*(data as *mut Inner<H>)).handler.read(input)
} else {
let data = data as usize & !1;
form::read(data as *mut c_void, input)
};
match ret {
Ok(s) => s,
Err(ReadError::Pause) => {
curl_sys::CURL_READFUNC_PAUSE
Expand Down
42 changes: 42 additions & 0 deletions tests/post.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
extern crate curl;

use std::io::Read;
use std::str;
use std::time::Duration;

Expand Down Expand Up @@ -106,3 +107,44 @@ hello\n\
t!(handle.httppost(form));
t!(handle.perform());
}

#[test]
fn stream() {
let s = Server::new();
s.receive("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:$PORT\r\n\
Accept: */*\r\n\
Content-Length: 238\r\n\
Expect: 100-continue\r\n\
Content-Type: multipart/form-data; boundary=--[..]\r\n\
\r\n\
--[..]\r\n\
Content-Disposition: form-data; name=\"foo\"\r\n\
\r\n\
1234\r\n\
--[..]\r\n\
Content-Disposition: form-data; name=\"bar\"\r\n\
\r\n\
5678\r\n\
--[..]\r\n\
");
s.send("HTTP/1.1 200 OK\r\n\r\n");

let mut handle = handle();
let mut form = Form::new();

let mut contents = "1234".as_bytes();
t!(form.part("foo").stream(Some(contents.len()), move |buf| {
Ok(contents.read(buf).unwrap())
}).add());

let mut contents = "5678".as_bytes();
t!(form.part("bar").stream(Some(contents.len()), move |buf| {
Ok(contents.read(buf).unwrap())
}).add());

t!(handle.url(&s.url("/")));
t!(handle.httppost(form));
t!(handle.perform());
}