Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
improved Mojo::Content::Single and Mojo::Content::MultiPart parsers t…
…o reuse events
  • Loading branch information
kraih committed Nov 10, 2011
1 parent 7d1c005 commit 979ae06
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 83 deletions.
2 changes: 2 additions & 0 deletions Changes
Expand Up @@ -2,6 +2,8 @@ This file documents the revision history for Perl extension Mojolicious.

2.26 2011-11-10 00:00:00
- Added EXPERIMENTAL upgrade event to Mojo::Asset::Memory.
- Improved Mojo::Content::Single and Mojo::Content::MultiPart parsers
to reuse events.
- Improved documentation.
- Fixed small route caching bug.

Expand Down
47 changes: 21 additions & 26 deletions lib/Mojo/Content.pm
Expand Up @@ -180,33 +180,28 @@ sub parse {
$self->{pre_buffer} = '';
}

# Custom body parser
if ($self->has_subscribers('read')) {
# Chunked or relaxed content
if ($self->is_chunked || $self->relaxed) {
$self->{size} += length($self->{buffer} //= '');
$self->emit(read => $self->{buffer});
$self->{buffer} = '';
}

# Chunked or relaxed content
if ($self->is_chunked || $self->relaxed) {
$self->emit(read => $self->{buffer} //= '');
$self->{buffer} = '';
# Normal content
else {
my $len = $self->headers->content_length || 0;
$self->{size} ||= 0;
my $need = $len - $self->{size};

# Slurp
if ($need > 0) {
my $chunk = substr $self->{buffer}, 0, $need, '';
$self->{size} += length $chunk;
$self->emit(read => $chunk);
}

# Normal content
else {

# Bytes needed
my $len = $self->headers->content_length || 0;
$self->{size} ||= 0;
my $need = $len - $self->{size};

# Slurp
if ($need > 0) {
my $chunk = substr $self->{buffer}, 0, $need, '';
$self->{size} = $self->{size} + length $chunk;
$self->emit(read => $chunk);
}

# Finished
$self->{state} = 'finished' if $len <= $self->progress;
}
# Finished
$self->{state} = 'finished' if $len <= $self->progress;
}

return $self;
Expand Down Expand Up @@ -440,9 +435,9 @@ Note that this event is EXPERIMENTAL and might change without warning!
my ($content, $chunk) = @_;
});
Emitted when a new chunk of content arrives, also disables normal content
storage in asset objects.
Emitted when a new chunk of content arrives.
$content->unsubscribe('read');
$content->on(read => sub {
my ($content, $chunk) = @_;
say "Streaming: $chunk";
Expand Down
51 changes: 29 additions & 22 deletions lib/Mojo/Content/MultiPart.pm
Expand Up @@ -5,6 +5,18 @@ use Mojo::Util 'b64_encode';

has parts => sub { [] };

sub new {
my $self = shift->SUPER::new(@_);
$self->on(
read => sub {
my ($self, $chunk) = @_;
$self->{multipart} .= $chunk;
$self->_parse_multipart;
}
);
return $self;
}

sub body_contains {
my ($self, $chunk) = @_;
for my $part (@{$self->parts}) {
Expand Down Expand Up @@ -122,12 +134,6 @@ sub get_body_chunk {

sub is_multipart {1}

sub parse {
my $self = shift;
$self->SUPER::parse(@_)->_parse_multipart;
return $self;
}

sub _parse_multipart {
my $self = shift;

Expand Down Expand Up @@ -157,19 +163,19 @@ sub _parse_multipart_body {
my ($self, $boundary) = @_;

# Whole part in buffer
my $pos = index $self->{buffer}, "\x0d\x0a--$boundary";
my $pos = index $self->{multipart}, "\x0d\x0a--$boundary";
if ($pos < 0) {
my $len = length($self->{buffer}) - (length($boundary) + 8);
my $len = length($self->{multipart}) - (length($boundary) + 8);
return unless $len > 0;

# Store chunk
my $chunk = substr $self->{buffer}, 0, $len, '';
my $chunk = substr $self->{multipart}, 0, $len, '';
$self->parts->[-1] = $self->parts->[-1]->parse($chunk);
return;
}

# Store chunk
my $chunk = substr $self->{buffer}, 0, $pos, '';
my $chunk = substr $self->{multipart}, 0, $pos, '';
$self->parts->[-1] = $self->parts->[-1]->parse($chunk);
$self->{multi_state} = 'multipart_boundary';
return 1;
Expand All @@ -179,8 +185,8 @@ sub _parse_multipart_boundary {
my ($self, $boundary) = @_;

# Boundary begins
if ((index $self->{buffer}, "\x0d\x0a--$boundary\x0d\x0a") == 0) {
substr $self->{buffer}, 0, length($boundary) + 6, '';
if ((index $self->{multipart}, "\x0d\x0a--$boundary\x0d\x0a") == 0) {
substr $self->{multipart}, 0, length($boundary) + 6, '';

# New part
$self->emit(part => my $part = Mojo::Content::Single->new(relaxed => 1));
Expand All @@ -191,8 +197,8 @@ sub _parse_multipart_boundary {

# Boundary ends
my $end = "\x0d\x0a--$boundary--";
if ((index $self->{buffer}, $end) == 0) {
substr $self->{buffer}, 0, length $end, '';
if ((index $self->{multipart}, $end) == 0) {
substr $self->{multipart}, 0, length $end, '';

# Finished
$self->{state} = $self->{multi_state} = 'finished';
Expand All @@ -205,9 +211,9 @@ sub _parse_multipart_preamble {
my ($self, $boundary) = @_;

# Replace preamble with carriage return and line feed
my $pos = index $self->{buffer}, "--$boundary";
my $pos = index $self->{multipart}, "--$boundary";
unless ($pos < 0) {
substr $self->{buffer}, 0, $pos, "\x0d\x0a";
substr $self->{multipart}, 0, $pos, "\x0d\x0a";

# Parse boundary
$self->{multi_state} = 'multipart_boundary';
Expand Down Expand Up @@ -276,6 +282,13 @@ L<Mojo::Content::Single> objects.
L<Mojo::Content::MultiPart> inherits all methods from L<Mojo::Content> and
implements the following new ones.
=head2 C<new>
my $multi = Mojo::Content::MultiPart->new;
Construct a new L<Mojo::Content::MultiPart> object and register default
C<read> event.
=head2 C<body_contains>
my $success = $multi->body_contains('foobarbaz');
Expand Down Expand Up @@ -313,12 +326,6 @@ Get a chunk of content starting from a specfic position.
True.
=head2 C<parse>
$multi = $multi->parse('Content-Type: multipart/mixed');
Parse content chunk.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.
Expand Down
48 changes: 23 additions & 25 deletions lib/Mojo/Content/Single.pm
Expand Up @@ -7,6 +7,17 @@ use Mojo::Content::MultiPart;
has asset => sub { Mojo::Asset::Memory->new };
has auto_upgrade => 1;

sub new {
my $self = shift->SUPER::new(@_);
$self->{read} = $self->on(
read => sub {
my ($self, $chunk) = @_;
$self->asset($self->asset->add_chunk($chunk));
}
);
return $self;
}

sub body_contains {
return 1 if shift->asset->contains(shift) >= 0;
return;
Expand Down Expand Up @@ -38,38 +49,18 @@ sub get_body_chunk {
sub parse {
my $self = shift;

# Parse headers and chunked body
$self->SUPER::parse(@_);

# Still parsing headers or using a custom body parser
return $self
if ($self->{state} || '') eq 'headers' || $self->has_subscribers('read');
# Parse headers
$self->parse_until_body(@_);

# Content needs to be upgraded to multipart
if ($self->auto_upgrade && defined($self->boundary)) {
$self->unsubscribe(read => $self->{read});
$self->emit(upgrade => my $multi = Mojo::Content::MultiPart->new($self));
return $multi->parse;
}

# Chunked body or relaxed content
my $asset = $self->asset;
if ($self->is_chunked || $self->relaxed) {
$self->asset($asset->add_chunk($self->{buffer}));
$self->{buffer} = '';
}

# Normal body
else {
my $len = $self->headers->content_length || 0;
my $need = $len - $asset->size;
$self->asset($asset->add_chunk(substr $self->{buffer}, 0, $need, ''))
if $need > 0;

# Finished
$self->{state} = 'finished' if $len <= $self->progress;
}

return $self;
# Parse body
return $self->SUPER::parse;
}

1;
Expand Down Expand Up @@ -137,6 +128,13 @@ Note that this attribute is EXPERIMENTAL and might change without warning!
L<Mojo::Content::Single> inherits all methods from L<Mojo::Content> and
implements the following new ones.
=head2 C<new>
my $single = Mojo::Content::Single->new;
Construct a new L<Mojo::Content::Single> object and register default C<read>
event.
=head2 C<body_contains>
my $success = $single->body_contains('1234567');
Expand Down
2 changes: 1 addition & 1 deletion lib/Mojolicious/Guides/Cookbook.pod
Expand Up @@ -457,7 +457,7 @@ L<Mojo::UserAgent> makes it actually easy.

my $ua = Mojo::UserAgent->new;
my $tx = $ua->build_tx(GET => 'http://mojolicio.us');
$tx->res->content->on(read => sub {
$tx->res->content->unsubscribe('read')->on(read => sub {
my ($content, $chunk) = @_;
say $chunk;
});
Expand Down
8 changes: 6 additions & 2 deletions t/mojo/request.t
Expand Up @@ -3,7 +3,7 @@ use Mojo::Base -strict;

use utf8;

use Test::More tests => 910;
use Test::More tests => 912;

# "When will I learn?
# The answer to life's problems aren't at the bottom of a bottle,
Expand Down Expand Up @@ -420,6 +420,8 @@ $ENV{MOJO_MAX_MESSAGE_SIZE} = $backup;

# Parse full HTTP 1.0 request
$req = Mojo::Message::Request->new;
my $body = '';
$req->content->on(read => sub { $body .= pop });
$req->parse('GET /foo/bar/baz.html?fo');
$req->parse("o=13#23 HTTP/1.0\x0d\x0aContent");
$req->parse('-Type: text/');
Expand All @@ -433,6 +435,8 @@ ok !$req->at_least_version('1.2'), 'not version 1.2';
is $req->url, '/foo/bar/baz.html?foo=13#23', 'right URL';
is $req->headers->content_type, 'text/plain', 'right "Content-Type" value';
is $req->headers->content_length, 27, 'right "Content-Length" value';
is $req->body, "Hello World!\n1234\nlalalala\n", 'right content';
is $body, "Hello World!\n1234\nlalalala\n", 'right content';

# Parse full HTTP 1.0 request (no scheme and empty elements in path)
$req = Mojo::Message::Request->new;
Expand Down Expand Up @@ -584,7 +588,7 @@ ok !$req->at_least_version('1.2'), 'not version 1.2';
is $req->url, '/foo/bar/baz.html?foo=13#23', 'right URL';
is $req->headers->content_length, 13, 'right "Content-Length" value';
is $req->headers->content_type, 'text/plain', 'right "Content-Type" value';
is $buffer, '131313abcd1313abcdefghi13', 'right content';
is $buffer, '131313abcd1313abcdefghi', 'right content';

# Parse HTTP 1.1 "x-application-urlencoded"
$req = Mojo::Message::Request->new;
Expand Down
14 changes: 7 additions & 7 deletions t/mojo/response.t
Expand Up @@ -542,23 +542,23 @@ $res->body('');
is $res->body, '', 'right content';
$res->body('hi there!');
is $res->body, 'hi there!', 'right content';
ok !$res->content->has_subscribers('read'), 'no subscribers';
is scalar @{$res->content->subscribers('read')}, 1, 'one subscriber';
$cb = $res->body(sub { });
ok $res->content->has_subscribers('read'), 'has subscribers';
is scalar @{$res->content->subscribers('read')}, 2, 'two subscribers';
$res->content->unsubscribe(read => $cb);
ok !$res->content->has_subscribers('read'), 'no subscribers';
is scalar @{$res->content->subscribers('read')}, 1, 'one subscriber';
$res->body('');
is $res->body, '', 'right content';
$res->body(0);
is $res->body, 0, 'right content';
ok !$res->content->has_subscribers('read'), 'no subscribers';
is scalar @{$res->content->subscribers('read')}, 1, 'one subscriber';
$cb = $res->body(sub { });
ok $res->content->has_subscribers('read'), 'has subscribers';
is scalar @{$res->content->subscribers('read')}, 2, 'two subscribers';
$res->content->unsubscribe(read => $cb);
$res->body('hello!');
ok !$res->content->has_subscribers('read'), 'no subscribers';
is scalar @{$res->content->subscribers('read')}, 1, 'one subscriber';
is $res->body, 'hello!', 'right content';
ok !$res->content->has_subscribers('read'), 'no subscribers';
is scalar @{$res->content->subscribers('read')}, 1, 'one subscriber';
$res->content(Mojo::Content::MultiPart->new);
$res->body('hi!');
is $res->body, 'hi!', 'right content';
Expand Down
1 change: 1 addition & 0 deletions t/mojolicious/upload_stream_lite_app.t
Expand Up @@ -43,6 +43,7 @@ post '/upload/:id' => sub {
body => sub {
my $single = shift;
return unless $single->headers->content_disposition =~ /my_file/;
$single->unsubscribe('read');
$single->on(read => sub { $cache->{$id} .= pop });
}
);
Expand Down

0 comments on commit 979ae06

Please sign in to comment.