2019-03-28 13:04:39 +01:00
|
|
|
//! Multipart payload support
|
|
|
|
use std::cell::{RefCell, UnsafeCell};
|
|
|
|
use std::marker::PhantomData;
|
|
|
|
use std::rc::Rc;
|
|
|
|
use std::{cmp, fmt};
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
use bytes::{Bytes, BytesMut};
|
2019-03-28 13:04:39 +01:00
|
|
|
use futures::task::{current as current_task, Task};
|
|
|
|
use futures::{Async, Poll, Stream};
|
|
|
|
use httparse;
|
|
|
|
use mime;
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
use actix_web::error::{ParseError, PayloadError};
|
|
|
|
use actix_web::http::header::{
|
2019-03-28 13:04:39 +01:00
|
|
|
self, ContentDisposition, HeaderMap, HeaderName, HeaderValue,
|
|
|
|
};
|
2019-04-03 21:28:58 +02:00
|
|
|
use actix_web::http::HttpTryFrom;
|
2019-03-28 13:04:39 +01:00
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
use crate::error::MultipartError;
|
2019-03-28 13:04:39 +01:00
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
const MAX_HEADERS: usize = 32;
|
2019-03-28 13:34:33 +01:00
|
|
|
|
2019-03-28 13:04:39 +01:00
|
|
|
/// The server-side implementation of `multipart/form-data` requests.
|
|
|
|
///
|
|
|
|
/// This will parse the incoming stream into `MultipartItem` instances via its
|
|
|
|
/// Stream implementation.
|
|
|
|
/// `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart`
|
|
|
|
/// is used for nested multipart streams.
|
|
|
|
pub struct Multipart {
|
|
|
|
safety: Safety,
|
|
|
|
error: Option<MultipartError>,
|
|
|
|
inner: Option<Rc<RefCell<InnerMultipart>>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Multipart item
|
2019-04-03 21:28:58 +02:00
|
|
|
pub enum Item {
|
2019-03-28 13:04:39 +01:00
|
|
|
/// Multipart field
|
2019-04-03 21:28:58 +02:00
|
|
|
Field(Field),
|
2019-03-28 13:04:39 +01:00
|
|
|
/// Nested multipart stream
|
|
|
|
Nested(Multipart),
|
|
|
|
}
|
|
|
|
|
|
|
|
enum InnerMultipartItem {
|
|
|
|
None,
|
|
|
|
Field(Rc<RefCell<InnerField>>),
|
|
|
|
Multipart(Rc<RefCell<InnerMultipart>>),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
enum InnerState {
|
|
|
|
/// Stream eof
|
|
|
|
Eof,
|
|
|
|
/// Skip data until first boundary
|
|
|
|
FirstBoundary,
|
|
|
|
/// Reading boundary
|
|
|
|
Boundary,
|
|
|
|
/// Reading Headers,
|
|
|
|
Headers,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct InnerMultipart {
|
|
|
|
payload: PayloadRef,
|
|
|
|
boundary: String,
|
|
|
|
state: InnerState,
|
|
|
|
item: InnerMultipartItem,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Multipart {
|
|
|
|
/// Create multipart instance for boundary.
|
|
|
|
pub fn new<S>(headers: &HeaderMap, stream: S) -> Multipart
|
|
|
|
where
|
|
|
|
S: Stream<Item = Bytes, Error = PayloadError> + 'static,
|
|
|
|
{
|
|
|
|
match Self::boundary(headers) {
|
|
|
|
Ok(boundary) => Multipart {
|
|
|
|
error: None,
|
|
|
|
safety: Safety::new(),
|
|
|
|
inner: Some(Rc::new(RefCell::new(InnerMultipart {
|
|
|
|
boundary,
|
2019-03-28 13:34:33 +01:00
|
|
|
payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))),
|
2019-03-28 13:04:39 +01:00
|
|
|
state: InnerState::FirstBoundary,
|
|
|
|
item: InnerMultipartItem::None,
|
|
|
|
}))),
|
|
|
|
},
|
|
|
|
Err(err) => Multipart {
|
|
|
|
error: Some(err),
|
|
|
|
safety: Safety::new(),
|
|
|
|
inner: None,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Extract boundary info from headers.
|
|
|
|
fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
|
|
|
|
if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
|
|
|
if let Ok(content_type) = content_type.to_str() {
|
|
|
|
if let Ok(ct) = content_type.parse::<mime::Mime>() {
|
|
|
|
if let Some(boundary) = ct.get_param(mime::BOUNDARY) {
|
|
|
|
Ok(boundary.as_str().to_owned())
|
|
|
|
} else {
|
|
|
|
Err(MultipartError::Boundary)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Err(MultipartError::ParseContentType)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Err(MultipartError::ParseContentType)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Err(MultipartError::NoContentType)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Stream for Multipart {
|
2019-04-03 21:28:58 +02:00
|
|
|
type Item = Item;
|
2019-03-28 13:04:39 +01:00
|
|
|
type Error = MultipartError;
|
|
|
|
|
|
|
|
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
|
|
|
if let Some(err) = self.error.take() {
|
|
|
|
Err(err)
|
|
|
|
} else if self.safety.current() {
|
2019-04-03 21:28:58 +02:00
|
|
|
let mut inner = self.inner.as_mut().unwrap().borrow_mut();
|
|
|
|
if let Some(payload) = inner.payload.get_mut(&self.safety) {
|
|
|
|
payload.poll_stream()?;
|
|
|
|
}
|
|
|
|
inner.poll(&self.safety)
|
2019-03-28 13:04:39 +01:00
|
|
|
} else {
|
|
|
|
Ok(Async::NotReady)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InnerMultipart {
|
2019-04-03 21:28:58 +02:00
|
|
|
fn read_headers(
|
|
|
|
payload: &mut PayloadBuffer,
|
|
|
|
) -> Result<Option<HeaderMap>, MultipartError> {
|
|
|
|
match payload.read_until(b"\r\n\r\n") {
|
|
|
|
None => {
|
|
|
|
if payload.eof {
|
|
|
|
Err(MultipartError::Incomplete)
|
|
|
|
} else {
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(bytes) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
|
|
|
|
match httparse::parse_headers(&bytes, &mut hdrs) {
|
|
|
|
Ok(httparse::Status::Complete((_, hdrs))) => {
|
|
|
|
// convert headers
|
|
|
|
let mut headers = HeaderMap::with_capacity(hdrs.len());
|
|
|
|
for h in hdrs {
|
|
|
|
if let Ok(name) = HeaderName::try_from(h.name) {
|
|
|
|
if let Ok(value) = HeaderValue::try_from(h.value) {
|
|
|
|
headers.append(name, value);
|
|
|
|
} else {
|
|
|
|
return Err(ParseError::Header.into());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(ParseError::Header.into());
|
|
|
|
}
|
|
|
|
}
|
2019-04-03 21:28:58 +02:00
|
|
|
Ok(Some(headers))
|
2019-03-28 13:04:39 +01:00
|
|
|
}
|
|
|
|
Ok(httparse::Status::Partial) => Err(ParseError::Header.into()),
|
|
|
|
Err(err) => Err(ParseError::from(err).into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn read_boundary(
|
|
|
|
payload: &mut PayloadBuffer,
|
|
|
|
boundary: &str,
|
2019-04-03 21:28:58 +02:00
|
|
|
) -> Result<Option<bool>, MultipartError> {
|
2019-03-28 13:04:39 +01:00
|
|
|
// TODO: need to read epilogue
|
2019-04-03 21:28:58 +02:00
|
|
|
match payload.readline() {
|
|
|
|
None => {
|
|
|
|
if payload.eof {
|
|
|
|
Err(MultipartError::Incomplete)
|
|
|
|
} else {
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(chunk) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
if chunk.len() == boundary.len() + 4
|
|
|
|
&& &chunk[..2] == b"--"
|
|
|
|
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
|
|
|
|
{
|
2019-04-03 21:28:58 +02:00
|
|
|
Ok(Some(false))
|
2019-03-28 13:04:39 +01:00
|
|
|
} else if chunk.len() == boundary.len() + 6
|
|
|
|
&& &chunk[..2] == b"--"
|
|
|
|
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
|
|
|
|
&& &chunk[boundary.len() + 2..boundary.len() + 4] == b"--"
|
|
|
|
{
|
2019-04-03 21:28:58 +02:00
|
|
|
Ok(Some(true))
|
2019-03-28 13:04:39 +01:00
|
|
|
} else {
|
|
|
|
Err(MultipartError::Boundary)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn skip_until_boundary(
|
|
|
|
payload: &mut PayloadBuffer,
|
|
|
|
boundary: &str,
|
2019-04-03 21:28:58 +02:00
|
|
|
) -> Result<Option<bool>, MultipartError> {
|
2019-03-28 13:04:39 +01:00
|
|
|
let mut eof = false;
|
|
|
|
loop {
|
2019-04-03 21:28:58 +02:00
|
|
|
match payload.readline() {
|
|
|
|
Some(chunk) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
if chunk.is_empty() {
|
|
|
|
//ValueError("Could not find starting boundary %r"
|
|
|
|
//% (self._boundary))
|
|
|
|
}
|
|
|
|
if chunk.len() < boundary.len() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if &chunk[..2] == b"--"
|
|
|
|
&& &chunk[2..chunk.len() - 2] == boundary.as_bytes()
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
if chunk.len() < boundary.len() + 2 {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let b: &[u8] = boundary.as_ref();
|
|
|
|
if &chunk[..boundary.len()] == b
|
|
|
|
&& &chunk[boundary.len()..boundary.len() + 2] == b"--"
|
|
|
|
{
|
|
|
|
eof = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-04-03 21:28:58 +02:00
|
|
|
None => {
|
|
|
|
return if payload.eof {
|
|
|
|
Err(MultipartError::Incomplete)
|
|
|
|
} else {
|
|
|
|
Ok(None)
|
|
|
|
};
|
|
|
|
}
|
2019-03-28 13:04:39 +01:00
|
|
|
}
|
|
|
|
}
|
2019-04-03 21:28:58 +02:00
|
|
|
Ok(Some(eof))
|
2019-03-28 13:04:39 +01:00
|
|
|
}
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
fn poll(&mut self, safety: &Safety) -> Poll<Option<Item>, MultipartError> {
|
2019-03-28 13:04:39 +01:00
|
|
|
if self.state == InnerState::Eof {
|
|
|
|
Ok(Async::Ready(None))
|
|
|
|
} else {
|
|
|
|
// release field
|
|
|
|
loop {
|
|
|
|
// Nested multipart streams of fields has to be consumed
|
|
|
|
// before switching to next
|
|
|
|
if safety.current() {
|
|
|
|
let stop = match self.item {
|
|
|
|
InnerMultipartItem::Field(ref mut field) => {
|
|
|
|
match field.borrow_mut().poll(safety)? {
|
|
|
|
Async::NotReady => return Ok(Async::NotReady),
|
|
|
|
Async::Ready(Some(_)) => continue,
|
|
|
|
Async::Ready(None) => true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
InnerMultipartItem::Multipart(ref mut multipart) => {
|
|
|
|
match multipart.borrow_mut().poll(safety)? {
|
|
|
|
Async::NotReady => return Ok(Async::NotReady),
|
|
|
|
Async::Ready(Some(_)) => continue,
|
|
|
|
Async::Ready(None) => true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => false,
|
|
|
|
};
|
|
|
|
if stop {
|
|
|
|
self.item = InnerMultipartItem::None;
|
|
|
|
}
|
|
|
|
if let InnerMultipartItem::None = self.item {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let headers = if let Some(payload) = self.payload.get_mut(safety) {
|
|
|
|
match self.state {
|
|
|
|
// read until first boundary
|
|
|
|
InnerState::FirstBoundary => {
|
|
|
|
match InnerMultipart::skip_until_boundary(
|
|
|
|
payload,
|
|
|
|
&self.boundary,
|
|
|
|
)? {
|
2019-04-03 21:28:58 +02:00
|
|
|
Some(eof) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
if eof {
|
|
|
|
self.state = InnerState::Eof;
|
|
|
|
return Ok(Async::Ready(None));
|
|
|
|
} else {
|
|
|
|
self.state = InnerState::Headers;
|
|
|
|
}
|
|
|
|
}
|
2019-04-03 21:28:58 +02:00
|
|
|
None => return Ok(Async::NotReady),
|
2019-03-28 13:04:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// read boundary
|
|
|
|
InnerState::Boundary => {
|
|
|
|
match InnerMultipart::read_boundary(payload, &self.boundary)? {
|
2019-04-03 21:28:58 +02:00
|
|
|
None => return Ok(Async::NotReady),
|
|
|
|
Some(eof) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
if eof {
|
|
|
|
self.state = InnerState::Eof;
|
|
|
|
return Ok(Async::Ready(None));
|
|
|
|
} else {
|
|
|
|
self.state = InnerState::Headers;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
// read field headers for next field
|
|
|
|
if self.state == InnerState::Headers {
|
2019-04-03 21:28:58 +02:00
|
|
|
if let Some(headers) = InnerMultipart::read_headers(payload)? {
|
2019-03-28 13:04:39 +01:00
|
|
|
self.state = InnerState::Boundary;
|
|
|
|
headers
|
|
|
|
} else {
|
|
|
|
return Ok(Async::NotReady);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
unreachable!()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log::debug!("NotReady: field is in flight");
|
|
|
|
return Ok(Async::NotReady);
|
|
|
|
};
|
|
|
|
|
|
|
|
// content type
|
|
|
|
let mut mt = mime::APPLICATION_OCTET_STREAM;
|
|
|
|
if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
|
|
|
if let Ok(content_type) = content_type.to_str() {
|
|
|
|
if let Ok(ct) = content_type.parse::<mime::Mime>() {
|
|
|
|
mt = ct;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
self.state = InnerState::Boundary;
|
|
|
|
|
|
|
|
// nested multipart stream
|
|
|
|
if mt.type_() == mime::MULTIPART {
|
|
|
|
let inner = if let Some(boundary) = mt.get_param(mime::BOUNDARY) {
|
|
|
|
Rc::new(RefCell::new(InnerMultipart {
|
|
|
|
payload: self.payload.clone(),
|
|
|
|
boundary: boundary.as_str().to_owned(),
|
|
|
|
state: InnerState::FirstBoundary,
|
|
|
|
item: InnerMultipartItem::None,
|
|
|
|
}))
|
|
|
|
} else {
|
|
|
|
return Err(MultipartError::Boundary);
|
|
|
|
};
|
|
|
|
|
|
|
|
self.item = InnerMultipartItem::Multipart(Rc::clone(&inner));
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
Ok(Async::Ready(Some(Item::Nested(Multipart {
|
2019-03-28 13:04:39 +01:00
|
|
|
safety: safety.clone(),
|
|
|
|
error: None,
|
|
|
|
inner: Some(inner),
|
|
|
|
}))))
|
|
|
|
} else {
|
|
|
|
let field = Rc::new(RefCell::new(InnerField::new(
|
|
|
|
self.payload.clone(),
|
|
|
|
self.boundary.clone(),
|
|
|
|
&headers,
|
|
|
|
)?));
|
|
|
|
self.item = InnerMultipartItem::Field(Rc::clone(&field));
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
Ok(Async::Ready(Some(Item::Field(Field::new(
|
|
|
|
safety.clone(),
|
|
|
|
headers,
|
|
|
|
mt,
|
|
|
|
field,
|
|
|
|
)))))
|
2019-03-28 13:04:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Drop for InnerMultipart {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
// InnerMultipartItem::Field has to be dropped first because of Safety.
|
|
|
|
self.item = InnerMultipartItem::None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A single field in a multipart stream
|
2019-04-03 21:28:58 +02:00
|
|
|
pub struct Field {
|
2019-03-28 13:04:39 +01:00
|
|
|
ct: mime::Mime,
|
|
|
|
headers: HeaderMap,
|
|
|
|
inner: Rc<RefCell<InnerField>>,
|
|
|
|
safety: Safety,
|
|
|
|
}
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
impl Field {
|
2019-03-28 13:04:39 +01:00
|
|
|
fn new(
|
|
|
|
safety: Safety,
|
|
|
|
headers: HeaderMap,
|
|
|
|
ct: mime::Mime,
|
|
|
|
inner: Rc<RefCell<InnerField>>,
|
|
|
|
) -> Self {
|
2019-04-03 21:28:58 +02:00
|
|
|
Field {
|
2019-03-28 13:04:39 +01:00
|
|
|
ct,
|
|
|
|
headers,
|
|
|
|
inner,
|
|
|
|
safety,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get a map of headers
|
|
|
|
pub fn headers(&self) -> &HeaderMap {
|
|
|
|
&self.headers
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the content type of the field
|
|
|
|
pub fn content_type(&self) -> &mime::Mime {
|
|
|
|
&self.ct
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the content disposition of the field, if it exists
|
|
|
|
pub fn content_disposition(&self) -> Option<ContentDisposition> {
|
|
|
|
// RFC 7578: 'Each part MUST contain a Content-Disposition header field
|
|
|
|
// where the disposition type is "form-data".'
|
|
|
|
if let Some(content_disposition) = self.headers.get(header::CONTENT_DISPOSITION)
|
|
|
|
{
|
|
|
|
ContentDisposition::from_raw(content_disposition).ok()
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
impl Stream for Field {
|
2019-03-28 13:04:39 +01:00
|
|
|
type Item = Bytes;
|
|
|
|
type Error = MultipartError;
|
|
|
|
|
|
|
|
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
|
|
|
if self.safety.current() {
|
2019-04-03 21:28:58 +02:00
|
|
|
let mut inner = self.inner.borrow_mut();
|
|
|
|
if let Some(payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety)
|
|
|
|
{
|
|
|
|
payload.poll_stream()?;
|
|
|
|
}
|
|
|
|
|
|
|
|
inner.poll(&self.safety)
|
2019-03-28 13:04:39 +01:00
|
|
|
} else {
|
|
|
|
Ok(Async::NotReady)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
impl fmt::Debug for Field {
|
2019-03-28 13:04:39 +01:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
2019-04-03 21:28:58 +02:00
|
|
|
writeln!(f, "\nField: {}", self.ct)?;
|
2019-03-28 13:04:39 +01:00
|
|
|
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
|
|
|
writeln!(f, " headers:")?;
|
|
|
|
for (key, val) in self.headers.iter() {
|
|
|
|
writeln!(f, " {:?}: {:?}", key, val)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct InnerField {
|
|
|
|
payload: Option<PayloadRef>,
|
|
|
|
boundary: String,
|
|
|
|
eof: bool,
|
|
|
|
length: Option<u64>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InnerField {
|
|
|
|
fn new(
|
|
|
|
payload: PayloadRef,
|
|
|
|
boundary: String,
|
|
|
|
headers: &HeaderMap,
|
|
|
|
) -> Result<InnerField, PayloadError> {
|
|
|
|
let len = if let Some(len) = headers.get(header::CONTENT_LENGTH) {
|
|
|
|
if let Ok(s) = len.to_str() {
|
|
|
|
if let Ok(len) = s.parse::<u64>() {
|
|
|
|
Some(len)
|
|
|
|
} else {
|
|
|
|
return Err(PayloadError::Incomplete(None));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(PayloadError::Incomplete(None));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(InnerField {
|
|
|
|
boundary,
|
|
|
|
payload: Some(payload),
|
|
|
|
eof: false,
|
|
|
|
length: len,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reads body part content chunk of the specified size.
|
|
|
|
/// The body part must has `Content-Length` header with proper value.
|
|
|
|
fn read_len(
|
|
|
|
payload: &mut PayloadBuffer,
|
|
|
|
size: &mut u64,
|
|
|
|
) -> Poll<Option<Bytes>, MultipartError> {
|
|
|
|
if *size == 0 {
|
|
|
|
Ok(Async::Ready(None))
|
|
|
|
} else {
|
2019-04-03 21:28:58 +02:00
|
|
|
match payload.read_max(*size) {
|
|
|
|
Some(mut chunk) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
let len = cmp::min(chunk.len() as u64, *size);
|
|
|
|
*size -= len;
|
|
|
|
let ch = chunk.split_to(len as usize);
|
|
|
|
if !chunk.is_empty() {
|
|
|
|
payload.unprocessed(chunk);
|
|
|
|
}
|
|
|
|
Ok(Async::Ready(Some(ch)))
|
|
|
|
}
|
2019-04-03 21:28:58 +02:00
|
|
|
None => {
|
|
|
|
if payload.eof && (*size != 0) {
|
|
|
|
Err(MultipartError::Incomplete)
|
|
|
|
} else {
|
|
|
|
Ok(Async::NotReady)
|
|
|
|
}
|
|
|
|
}
|
2019-03-28 13:04:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Reads content chunk of body part with unknown length.
|
|
|
|
/// The `Content-Length` header for body part is not necessary.
|
|
|
|
fn read_stream(
|
|
|
|
payload: &mut PayloadBuffer,
|
|
|
|
boundary: &str,
|
|
|
|
) -> Poll<Option<Bytes>, MultipartError> {
|
2019-04-03 21:28:58 +02:00
|
|
|
match payload.read_until(b"\r") {
|
|
|
|
None => {
|
|
|
|
if payload.eof {
|
|
|
|
Err(MultipartError::Incomplete)
|
|
|
|
} else {
|
|
|
|
Ok(Async::NotReady)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(mut chunk) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
if chunk.len() == 1 {
|
|
|
|
payload.unprocessed(chunk);
|
2019-04-03 21:28:58 +02:00
|
|
|
match payload.read_exact(boundary.len() + 4) {
|
|
|
|
None => {
|
|
|
|
if payload.eof {
|
|
|
|
Err(MultipartError::Incomplete)
|
|
|
|
} else {
|
|
|
|
Ok(Async::NotReady)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(mut chunk) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
if &chunk[..2] == b"\r\n"
|
|
|
|
&& &chunk[2..4] == b"--"
|
|
|
|
&& &chunk[4..] == boundary.as_bytes()
|
|
|
|
{
|
|
|
|
payload.unprocessed(chunk);
|
|
|
|
Ok(Async::Ready(None))
|
|
|
|
} else {
|
|
|
|
// \r might be part of data stream
|
|
|
|
let ch = chunk.split_to(1);
|
|
|
|
payload.unprocessed(chunk);
|
|
|
|
Ok(Async::Ready(Some(ch)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let to = chunk.len() - 1;
|
|
|
|
let ch = chunk.split_to(to);
|
|
|
|
payload.unprocessed(chunk);
|
|
|
|
Ok(Async::Ready(Some(ch)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn poll(&mut self, s: &Safety) -> Poll<Option<Bytes>, MultipartError> {
|
|
|
|
if self.payload.is_none() {
|
|
|
|
return Ok(Async::Ready(None));
|
|
|
|
}
|
|
|
|
|
|
|
|
let result = if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) {
|
|
|
|
let res = if let Some(ref mut len) = self.length {
|
|
|
|
InnerField::read_len(payload, len)?
|
|
|
|
} else {
|
|
|
|
InnerField::read_stream(payload, &self.boundary)?
|
|
|
|
};
|
|
|
|
|
|
|
|
match res {
|
|
|
|
Async::NotReady => Async::NotReady,
|
|
|
|
Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)),
|
|
|
|
Async::Ready(None) => {
|
|
|
|
self.eof = true;
|
2019-04-03 21:28:58 +02:00
|
|
|
match payload.readline() {
|
|
|
|
None => Async::Ready(None),
|
|
|
|
Some(line) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
if line.as_ref() != b"\r\n" {
|
|
|
|
log::warn!("multipart field did not read all the data or it is malformed");
|
|
|
|
}
|
|
|
|
Async::Ready(None)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Async::NotReady
|
|
|
|
};
|
|
|
|
|
|
|
|
if Async::Ready(None) == result {
|
|
|
|
self.payload.take();
|
|
|
|
}
|
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct PayloadRef {
|
|
|
|
payload: Rc<UnsafeCell<PayloadBuffer>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PayloadRef {
|
|
|
|
fn new(payload: PayloadBuffer) -> PayloadRef {
|
|
|
|
PayloadRef {
|
|
|
|
payload: Rc::new(payload.into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<&'a mut PayloadBuffer>
|
|
|
|
where
|
|
|
|
'a: 'b,
|
|
|
|
{
|
|
|
|
// Unsafe: Invariant is inforced by Safety Safety is used as ref counter,
|
|
|
|
// only top most ref can have mutable access to payload.
|
|
|
|
if s.current() {
|
|
|
|
let payload: &mut PayloadBuffer = unsafe { &mut *self.payload.get() };
|
|
|
|
Some(payload)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Clone for PayloadRef {
|
|
|
|
fn clone(&self) -> PayloadRef {
|
|
|
|
PayloadRef {
|
|
|
|
payload: Rc::clone(&self.payload),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Counter. It tracks of number of clones of payloads and give access to
|
|
|
|
/// payload only to top most task panics if Safety get destroyed and it not top
|
|
|
|
/// most task.
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Safety {
|
|
|
|
task: Option<Task>,
|
|
|
|
level: usize,
|
|
|
|
payload: Rc<PhantomData<bool>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Safety {
|
|
|
|
fn new() -> Safety {
|
|
|
|
let payload = Rc::new(PhantomData);
|
|
|
|
Safety {
|
|
|
|
task: None,
|
|
|
|
level: Rc::strong_count(&payload),
|
|
|
|
payload,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn current(&self) -> bool {
|
|
|
|
Rc::strong_count(&self.payload) == self.level
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Clone for Safety {
|
|
|
|
fn clone(&self) -> Safety {
|
|
|
|
let payload = Rc::clone(&self.payload);
|
|
|
|
Safety {
|
|
|
|
task: Some(current_task()),
|
|
|
|
level: Rc::strong_count(&payload),
|
|
|
|
payload,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Drop for Safety {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
// parent task is dead
|
|
|
|
if Rc::strong_count(&self.payload) != self.level {
|
|
|
|
panic!("Safety get dropped but it is not from top-most task");
|
|
|
|
}
|
|
|
|
if let Some(task) = self.task.take() {
|
|
|
|
task.notify()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
/// Payload buffer
|
|
|
|
struct PayloadBuffer {
|
|
|
|
eof: bool,
|
|
|
|
buf: BytesMut,
|
|
|
|
stream: Box<dyn Stream<Item = Bytes, Error = PayloadError>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PayloadBuffer {
|
|
|
|
/// Create new `PayloadBuffer` instance
|
|
|
|
fn new<S>(stream: S) -> Self
|
|
|
|
where
|
|
|
|
S: Stream<Item = Bytes, Error = PayloadError> + 'static,
|
|
|
|
{
|
|
|
|
PayloadBuffer {
|
|
|
|
eof: false,
|
|
|
|
buf: BytesMut::new(),
|
|
|
|
stream: Box::new(stream),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn poll_stream(&mut self) -> Result<(), PayloadError> {
|
|
|
|
loop {
|
|
|
|
match self.stream.poll()? {
|
|
|
|
Async::Ready(Some(data)) => self.buf.extend_from_slice(&data),
|
|
|
|
Async::Ready(None) => {
|
|
|
|
self.eof = true;
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
Async::NotReady => return Ok(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Read exact number of bytes
|
|
|
|
#[inline]
|
|
|
|
fn read_exact(&mut self, size: usize) -> Option<Bytes> {
|
|
|
|
if size <= self.buf.len() {
|
|
|
|
Some(self.buf.split_to(size).freeze())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn read_max(&mut self, size: u64) -> Option<Bytes> {
|
|
|
|
if !self.buf.is_empty() {
|
|
|
|
let size = std::cmp::min(self.buf.len() as u64, size) as usize;
|
|
|
|
Some(self.buf.split_to(size).freeze())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Read until specified ending
|
|
|
|
pub fn read_until(&mut self, line: &[u8]) -> Option<Bytes> {
|
|
|
|
twoway::find_bytes(&self.buf, line)
|
|
|
|
.map(|idx| self.buf.split_to(idx + line.len()).freeze())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Read bytes until new line delimiter
|
|
|
|
pub fn readline(&mut self) -> Option<Bytes> {
|
|
|
|
self.read_until(b"\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Put unprocessed data back to the buffer
|
|
|
|
pub fn unprocessed(&mut self, data: Bytes) {
|
|
|
|
let buf = BytesMut::from(data);
|
|
|
|
let buf = std::mem::replace(&mut self.buf, buf);
|
|
|
|
self.buf.extend_from_slice(&buf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-28 13:04:39 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2019-04-03 21:28:58 +02:00
|
|
|
use actix_http::h1::{Payload, PayloadWriter};
|
2019-03-28 13:04:39 +01:00
|
|
|
use bytes::Bytes;
|
|
|
|
use futures::unsync::mpsc;
|
|
|
|
|
|
|
|
use super::*;
|
2019-04-03 21:28:58 +02:00
|
|
|
use actix_web::http::header::{DispositionParam, DispositionType};
|
|
|
|
use actix_web::test::run_on;
|
2019-03-28 13:04:39 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_boundary() {
|
|
|
|
let headers = HeaderMap::new();
|
|
|
|
match Multipart::boundary(&headers) {
|
|
|
|
Err(MultipartError::NoContentType) => (),
|
|
|
|
_ => unreachable!("should not happen"),
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
headers.insert(
|
|
|
|
header::CONTENT_TYPE,
|
|
|
|
header::HeaderValue::from_static("test"),
|
|
|
|
);
|
|
|
|
|
|
|
|
match Multipart::boundary(&headers) {
|
|
|
|
Err(MultipartError::ParseContentType) => (),
|
|
|
|
_ => unreachable!("should not happen"),
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
headers.insert(
|
|
|
|
header::CONTENT_TYPE,
|
|
|
|
header::HeaderValue::from_static("multipart/mixed"),
|
|
|
|
);
|
|
|
|
match Multipart::boundary(&headers) {
|
|
|
|
Err(MultipartError::Boundary) => (),
|
|
|
|
_ => unreachable!("should not happen"),
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
headers.insert(
|
|
|
|
header::CONTENT_TYPE,
|
|
|
|
header::HeaderValue::from_static(
|
|
|
|
"multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"",
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
Multipart::boundary(&headers).unwrap(),
|
|
|
|
"5c02368e880e436dab70ed54e1c58209"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn create_stream() -> (
|
|
|
|
mpsc::UnboundedSender<Result<Bytes, PayloadError>>,
|
|
|
|
impl Stream<Item = Bytes, Error = PayloadError>,
|
|
|
|
) {
|
|
|
|
let (tx, rx) = mpsc::unbounded();
|
|
|
|
|
|
|
|
(tx, rx.map_err(|_| panic!()).and_then(|res| res))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_multipart() {
|
|
|
|
run_on(|| {
|
|
|
|
let (sender, payload) = create_stream();
|
|
|
|
|
|
|
|
let bytes = Bytes::from(
|
|
|
|
"testasdadsad\r\n\
|
|
|
|
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
|
|
|
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
|
|
|
|
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
|
|
|
test\r\n\
|
|
|
|
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
|
|
|
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
|
|
|
data\r\n\
|
|
|
|
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
|
|
|
|
);
|
|
|
|
sender.unbounded_send(Ok(bytes)).unwrap();
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
headers.insert(
|
|
|
|
header::CONTENT_TYPE,
|
|
|
|
header::HeaderValue::from_static(
|
|
|
|
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut multipart = Multipart::new(&headers, payload);
|
2019-04-03 21:28:58 +02:00
|
|
|
match multipart.poll().unwrap() {
|
|
|
|
Async::Ready(Some(item)) => match item {
|
|
|
|
Item::Field(mut field) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
{
|
|
|
|
let cd = field.content_disposition().unwrap();
|
|
|
|
assert_eq!(cd.disposition, DispositionType::FormData);
|
|
|
|
assert_eq!(
|
|
|
|
cd.parameters[0],
|
|
|
|
DispositionParam::Name("file".into())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
|
|
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
match field.poll().unwrap() {
|
|
|
|
Async::Ready(Some(chunk)) => assert_eq!(chunk, "test"),
|
2019-03-28 13:04:39 +01:00
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
2019-04-03 21:28:58 +02:00
|
|
|
match field.poll().unwrap() {
|
|
|
|
Async::Ready(None) => (),
|
2019-03-28 13:04:39 +01:00
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
|
|
},
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
match multipart.poll().unwrap() {
|
|
|
|
Async::Ready(Some(item)) => match item {
|
|
|
|
Item::Field(mut field) => {
|
2019-03-28 13:04:39 +01:00
|
|
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
|
|
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
|
|
|
|
|
|
|
match field.poll() {
|
|
|
|
Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "data"),
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
match field.poll() {
|
|
|
|
Ok(Async::Ready(None)) => (),
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
|
|
},
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
|
2019-04-03 21:28:58 +02:00
|
|
|
match multipart.poll().unwrap() {
|
|
|
|
Async::Ready(None) => (),
|
2019-03-28 13:04:39 +01:00
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-04-03 21:28:58 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_basic() {
|
|
|
|
run_on(|| {
|
|
|
|
let (_, payload) = Payload::create(false);
|
|
|
|
let mut payload = PayloadBuffer::new(payload);
|
|
|
|
|
|
|
|
assert_eq!(payload.buf.len(), 0);
|
|
|
|
payload.poll_stream().unwrap();
|
|
|
|
assert_eq!(None, payload.read_max(1));
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_eof() {
|
|
|
|
run_on(|| {
|
|
|
|
let (mut sender, payload) = Payload::create(false);
|
|
|
|
let mut payload = PayloadBuffer::new(payload);
|
|
|
|
|
|
|
|
assert_eq!(None, payload.read_max(4));
|
|
|
|
sender.feed_data(Bytes::from("data"));
|
|
|
|
sender.feed_eof();
|
|
|
|
payload.poll_stream().unwrap();
|
|
|
|
|
|
|
|
assert_eq!(Some(Bytes::from("data")), payload.read_max(4));
|
|
|
|
assert_eq!(payload.buf.len(), 0);
|
|
|
|
assert_eq!(None, payload.read_max(1));
|
|
|
|
assert!(payload.eof);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_err() {
|
|
|
|
run_on(|| {
|
|
|
|
let (mut sender, payload) = Payload::create(false);
|
|
|
|
let mut payload = PayloadBuffer::new(payload);
|
|
|
|
assert_eq!(None, payload.read_max(1));
|
|
|
|
sender.set_error(PayloadError::Incomplete(None));
|
|
|
|
payload.poll_stream().err().unwrap();
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_readmax() {
|
|
|
|
run_on(|| {
|
|
|
|
let (mut sender, payload) = Payload::create(false);
|
|
|
|
let mut payload = PayloadBuffer::new(payload);
|
|
|
|
|
|
|
|
sender.feed_data(Bytes::from("line1"));
|
|
|
|
sender.feed_data(Bytes::from("line2"));
|
|
|
|
payload.poll_stream().unwrap();
|
|
|
|
assert_eq!(payload.buf.len(), 10);
|
|
|
|
|
|
|
|
assert_eq!(Some(Bytes::from("line1")), payload.read_max(5));
|
|
|
|
assert_eq!(payload.buf.len(), 5);
|
|
|
|
|
|
|
|
assert_eq!(Some(Bytes::from("line2")), payload.read_max(5));
|
|
|
|
assert_eq!(payload.buf.len(), 0);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_readexactly() {
|
|
|
|
run_on(|| {
|
|
|
|
let (mut sender, payload) = Payload::create(false);
|
|
|
|
let mut payload = PayloadBuffer::new(payload);
|
|
|
|
|
|
|
|
assert_eq!(None, payload.read_exact(2));
|
|
|
|
|
|
|
|
sender.feed_data(Bytes::from("line1"));
|
|
|
|
sender.feed_data(Bytes::from("line2"));
|
|
|
|
payload.poll_stream().unwrap();
|
|
|
|
|
|
|
|
assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2));
|
|
|
|
assert_eq!(payload.buf.len(), 8);
|
|
|
|
|
|
|
|
assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4));
|
|
|
|
assert_eq!(payload.buf.len(), 4);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_readuntil() {
|
|
|
|
run_on(|| {
|
|
|
|
let (mut sender, payload) = Payload::create(false);
|
|
|
|
let mut payload = PayloadBuffer::new(payload);
|
|
|
|
|
|
|
|
assert_eq!(None, payload.read_until(b"ne"));
|
|
|
|
|
|
|
|
sender.feed_data(Bytes::from("line1"));
|
|
|
|
sender.feed_data(Bytes::from("line2"));
|
|
|
|
payload.poll_stream().unwrap();
|
|
|
|
|
|
|
|
assert_eq!(Some(Bytes::from("line")), payload.read_until(b"ne"));
|
|
|
|
assert_eq!(payload.buf.len(), 6);
|
|
|
|
|
|
|
|
assert_eq!(Some(Bytes::from("1line2")), payload.read_until(b"2"));
|
|
|
|
assert_eq!(payload.buf.len(), 0);
|
|
|
|
})
|
|
|
|
}
|
2019-03-28 13:04:39 +01:00
|
|
|
}
|