From 0596ebbfd0202f984d2a42bcfd62879c73968d08 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 9 May 2017 15:58:12 -0700 Subject: [PATCH] Add support for CURLFORM_STREAM Closes #158 --- src/easy/form.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++- src/easy/handler.rs | 14 ++++++++++- tests/post.rs | 42 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/easy/form.rs b/src/easy/form.rs index 865081e7fa..250cbf8f4b 100644 --- a/src/easy/form.rs +++ b/src/easy/form.rs @@ -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. /// @@ -16,6 +19,7 @@ pub struct Form { headers: Vec, buffers: Vec>, strings: Vec, + streams: Option>, } /// One part in a multipart upload, added to a `Form`. @@ -26,10 +30,19 @@ pub struct Part<'form, 'data> { error: Option, } +struct StreamNode { + stream: Box Result>, + _next: Option>, +} + pub fn raw(form: &Form) -> *mut curl_sys::curl_httppost { form.head } +pub unsafe fn read(raw: *mut c_void, into: &mut [u8]) -> Result { + ((*(raw as *mut StreamNode)).stream)(into) +} + impl Form { /// Creates a new blank form ready for the addition of new data. pub fn new() -> Form { @@ -39,6 +52,7 @@ impl Form { headers: Vec::new(), buffers: Vec::new(), strings: Vec::new(), + streams: None, } } @@ -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(&mut self, + len: Option, + f: F) -> &mut Self + where F: FnMut(&mut [u8]) -> Result + 'static + Send, + { + self._stream(len, Box::new(f)); + self + } + + fn _stream(&mut self, + len: Option, + stream: Box Result + 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 diff --git a/src/easy/handler.rs b/src/easy/handler.rs index dde73aa7a4..60b62cf898 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -613,6 +613,7 @@ impl Easy2 { let cb: curl_sys::curl_read_callback = read_cb::; 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"); @@ -2783,7 +2784,18 @@ extern fn read_cb(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)).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)).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 diff --git a/tests/post.rs b/tests/post.rs index 8b3413b24f..aa532ebbb5 100644 --- a/tests/post.rs +++ b/tests/post.rs @@ -1,5 +1,6 @@ extern crate curl; +use std::io::Read; use std::str; use std::time::Duration; @@ -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()); +}