Skip to content

Commit 0349a5e

Browse files
committed
unit test for chunking stream
1 parent 52b64a0 commit 0349a5e

File tree

2 files changed

+161
-129
lines changed

2 files changed

+161
-129
lines changed

src/aws-cpp-sdk-core/include/smithy/client/features/ChunkingInterceptor.h

Lines changed: 88 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,77 +2,89 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0.
44
*/
5-
65
#pragma once
7-
8-
#include <aws/core/Core_EXPORTS.h>
9-
#include <aws/core/client/ClientConfiguration.h>
10-
#include <aws/core/utils/HashingUtils.h>
11-
#include <aws/core/utils/StringUtils.h>
6+
#include <aws/core/http/HttpRequest.h>
127
#include <aws/core/utils/Array.h>
8+
#include <aws/core/utils/StringUtils.h>
9+
#include <aws/core/utils/HashingUtils.h>
10+
#include <aws/core/utils/logging/LogMacros.h>
11+
#include <aws/core/utils/memory/stl/AWSStringStream.h>
1312
#include <smithy/interceptor/Interceptor.h>
14-
#include <smithy/client/common/AwsSmithyClientUtils.h>
13+
#include <aws/core/client/ClientConfiguration.h>
14+
#include <aws/core/utils/Outcome.h>
15+
#include <aws/core/client/AWSError.h>
16+
#include <memory>
1517

1618
namespace smithy {
1719
namespace client {
1820
namespace features {
1921

20-
namespace {
21-
22-
static const char* const CHECKSUM_HEADER_PREFIX = "x-amz-checksum-";
23-
static const char* const ALLOCATION_TAG = "ChunkingInterceptor";
24-
static const size_t DATA_BUFFER_SIZE = 65536;
25-
26-
} // anonymous namespace
22+
static const size_t AWS_DATA_BUFFER_SIZE = 65536;
23+
static const char* ALLOCATION_TAG = "ChunkingInterceptor";
24+
static const char* CHECKSUM_HEADER_PREFIX = "x-amz-checksum-";
2725

28-
class AwsChunkedStreamWrapper {
26+
template <size_t DataBufferSize = AWS_DATA_BUFFER_SIZE>
27+
class AwsChunkedStreamBuf : public std::streambuf {
2928
public:
30-
AwsChunkedStreamWrapper(Aws::Http::HttpRequest* request, const std::shared_ptr<Aws::IOStream>& originalBody, size_t bufferSize = DATA_BUFFER_SIZE)
31-
: m_streambuf(request, originalBody, bufferSize), m_iostream(&m_streambuf) {}
32-
33-
Aws::IOStream* GetIOStream() { return &m_iostream; }
29+
AwsChunkedStreamBuf(Aws::Http::HttpRequest* request,
30+
const std::shared_ptr<Aws::IOStream>& stream,
31+
size_t bufferSize = DataBufferSize)
32+
: m_chunkingStream(Aws::MakeShared<Aws::StringStream>("AwsChunkedStream")),
33+
m_request(request),
34+
m_stream(stream),
35+
m_data(bufferSize)
36+
{
37+
assert(m_stream != nullptr);
38+
if (m_stream == nullptr) {
39+
AWS_LOGSTREAM_ERROR("AwsChunkedStream", "stream is null");
40+
}
41+
assert(m_request != nullptr);
42+
if (m_request == nullptr) {
43+
AWS_LOGSTREAM_ERROR("AwsChunkedStream", "request is null");
44+
}
3445

35-
private:
36-
class AwsChunkedStreamBuf : public std::streambuf {
37-
public:
38-
AwsChunkedStreamBuf(Aws::Http::HttpRequest* request, const std::shared_ptr<Aws::IOStream>& originalBody, size_t bufferSize)
39-
: m_request(request), m_stream(originalBody), m_data(bufferSize), m_bufferSize(bufferSize),
40-
m_chunkingStream(Aws::MakeShared<Aws::StringStream>(ALLOCATION_TAG)) {
41-
setg(nullptr, nullptr, nullptr);
46+
setg(nullptr, nullptr, nullptr);
47+
}
48+
49+
protected:
50+
int_type underflow() override {
51+
if (gptr() && gptr() < egptr()) {
52+
return traits_type::to_int_type(*gptr());
4253
}
4354

44-
protected:
45-
int_type underflow() override {
46-
if (gptr() < egptr()) {
47-
return traits_type::to_int_type(*gptr());
55+
// only read and write to chunked stream if the underlying stream
56+
// is still in a valid state
57+
if (m_stream->good()) {
58+
// Try to read in a 64K chunk, if we cant we know the stream is over
59+
m_stream->read(m_data.GetUnderlyingData(), m_data.GetLength());
60+
size_t bytesRead = static_cast<size_t>(m_stream->gcount());
61+
writeChunk(bytesRead);
62+
63+
// if we've read everything from the stream, we want to add the trailer
64+
// to the underlying stream
65+
if ((m_stream->peek() == EOF || m_stream->eof()) && !m_stream->bad()) {
66+
writeTrailerToUnderlyingStream();
4867
}
68+
}
4969

50-
if (m_stream->good()) {
51-
m_stream->read(m_data.GetUnderlyingData(), m_bufferSize);
52-
size_t bytesRead = static_cast<size_t>(m_stream->gcount());
53-
writeChunk(bytesRead);
54-
55-
if ((m_stream->peek() == EOF || m_stream->eof()) && !m_stream->bad()) {
56-
writeTrailerToUnderlyingStream();
57-
}
58-
}
59-
60-
if ((m_chunkingStream->peek() == EOF || m_chunkingStream->eof()) && !m_chunkingStream->bad()) {
61-
return traits_type::eof();
62-
}
63-
64-
m_chunkingStream->read(m_buffer, sizeof(m_buffer));
65-
size_t bytesRead = static_cast<size_t>(m_chunkingStream->gcount());
66-
if (bytesRead == 0) {
67-
return traits_type::eof();
68-
}
69-
70-
setg(m_buffer, m_buffer, m_buffer + bytesRead);
71-
return traits_type::to_int_type(*gptr());
70+
// if the underlying stream is empty there is nothing to read
71+
if ((m_chunkingStream->peek() == EOF || m_chunkingStream->eof()) && !m_chunkingStream->bad()) {
72+
return traits_type::eof();
7273
}
7374

74-
private:
75-
void writeTrailerToUnderlyingStream() {
75+
// Read from chunking stream to internal buffer
76+
m_chunkingStream->read(m_buffer.GetUnderlyingData(), m_buffer.GetLength());
77+
size_t bytesRead = static_cast<size_t>(m_chunkingStream->gcount());
78+
if (bytesRead == 0) {
79+
return traits_type::eof();
80+
}
81+
82+
setg(m_buffer.GetUnderlyingData(), m_buffer.GetUnderlyingData(), m_buffer.GetUnderlyingData() + bytesRead);
83+
return traits_type::to_int_type(*gptr());
84+
}
85+
86+
private:
87+
void writeTrailerToUnderlyingStream() {
7688
Aws::StringStream chunkedTrailerStream;
7789
chunkedTrailerStream << "0\r\n";
7890
if (m_request->GetRequestHash().second != nullptr) {
@@ -86,13 +98,13 @@ class AwsChunkedStreamWrapper {
8698
}
8799
*m_chunkingStream << chunkedTrailer;
88100
}
89-
101+
90102
void writeChunk(size_t bytesRead) {
91103
if (m_request->GetRequestHash().second != nullptr) {
92104
m_request->GetRequestHash().second->Update(reinterpret_cast<unsigned char*>(m_data.GetUnderlyingData()), bytesRead);
93105
}
94-
95-
if (bytesRead > 0 && m_chunkingStream != nullptr && !m_chunkingStream->bad()) {
106+
107+
if (bytesRead > 0 && m_chunkingStream && !m_chunkingStream->bad()) {
96108
if (m_chunkingStream->eof()) {
97109
m_chunkingStream->clear();
98110
}
@@ -102,16 +114,23 @@ class AwsChunkedStreamWrapper {
102114
}
103115
}
104116

105-
Aws::Http::HttpRequest* m_request;
106-
std::shared_ptr<Aws::IOStream> m_stream;
107-
Aws::Utils::Array<char> m_data;
108-
size_t m_bufferSize;
109-
std::shared_ptr<Aws::IOStream> m_chunkingStream;
110-
char m_buffer[8192];
111-
};
112-
113-
AwsChunkedStreamBuf m_streambuf;
114-
Aws::IOStream m_iostream;
117+
std::shared_ptr<Aws::IOStream> m_chunkingStream;
118+
Aws::Http::HttpRequest* m_request{nullptr};
119+
std::shared_ptr<Aws::IOStream> m_stream;
120+
Aws::Utils::Array<char> m_data;
121+
Aws::Utils::Array<char> m_buffer{DataBufferSize};
122+
};
123+
124+
class AwsChunkedIOStream : public Aws::IOStream {
125+
public:
126+
AwsChunkedIOStream(Aws::Http::HttpRequest* request,
127+
const std::shared_ptr<Aws::IOStream>& originalBody,
128+
size_t bufferSize = AWS_DATA_BUFFER_SIZE)
129+
: Aws::IOStream(&m_buf),
130+
m_buf(request, originalBody, bufferSize) {}
131+
132+
private:
133+
AwsChunkedStreamBuf<> m_buf;
115134
};
116135

117136
/**
@@ -159,11 +178,9 @@ class ChunkingInterceptor : public smithy::interceptor::Interceptor {
159178
}
160179
}
161180

162-
auto wrapper = Aws::MakeShared<AwsChunkedStreamWrapper>(
181+
auto chunkedBody = Aws::MakeShared<AwsChunkedIOStream>(
163182
ALLOCATION_TAG, request.get(), originalBody);
164-
auto chunkedBody = std::shared_ptr<Aws::IOStream>(
165-
wrapper, wrapper->GetIOStream());
166-
183+
167184
request->AddContentBody(chunkedBody);
168185
return request;
169186
}

tests/aws-cpp-sdk-core-tests/utils/stream/ChunkingInterceptorTest.cpp

Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,76 +12,91 @@ using namespace Aws::Http::Standard;
1212
using namespace smithy::client::features;
1313
using namespace Aws::Utils::Crypto;
1414

15-
class ChunkingInterceptorTest : public Aws::Testing::AwsCppSdkGTestSuite {};
16-
1715
const char* CHUNKING_TEST_LOG_TAG = "CHUNKING_INTERCEPTOR_TEST";
1816

19-
// Helper function to read all data from a stream
20-
std::string ReadAllFromStream(std::shared_ptr<Aws::IOStream> stream) {
21-
std::stringstream result;
22-
char buffer[1024];
23-
while (stream->good() && !stream->eof()) {
24-
stream->read(buffer, sizeof(buffer));
25-
auto bytesRead = stream->gcount();
26-
if (bytesRead > 0) {
27-
result.write(buffer, bytesRead);
28-
}
17+
class ChunkingInterceptorTest : public Aws::Testing::AwsCppSdkGTestSuite {
18+
protected:
19+
template <typename Fn>
20+
void withChunkedStream(const std::string& input, size_t bufferSize, Fn&& fn) {
21+
StandardHttpRequest request{"test.com", Http::HttpMethod::HTTP_GET};
22+
auto requestHash = Aws::MakeShared<CRC32>(CHUNKING_TEST_LOG_TAG);
23+
request.SetRequestHash("crc32", requestHash);
24+
auto inputStream = Aws::MakeShared<Aws::StringStream>(CHUNKING_TEST_LOG_TAG, input);
25+
26+
AwsChunkedIOStream wrapper{&request, inputStream, bufferSize};
27+
Aws::IOStream* stream = &wrapper;
28+
29+
fn(*stream);
2930
}
30-
return result.str();
31-
}
31+
};
3232

3333
TEST_F(ChunkingInterceptorTest, ChunkedStreamShouldWork) {
34-
StandardHttpRequest request{"www.elda.com/will", Http::HttpMethod::HTTP_GET};
35-
auto requestHash = Aws::MakeShared<CRC32>(CHUNKING_TEST_LOG_TAG);
36-
request.SetRequestHash("crc32", requestHash);
37-
std::shared_ptr<Aws::IOStream> inputStream = Aws::MakeShared<Aws::StringStream>(CHUNKING_TEST_LOG_TAG, "1234567890123456789012345");
38-
39-
AwsChunkedStreamWrapper wrapper{&request, inputStream, 10};
40-
auto chunkedStream = wrapper.GetIOStream();
41-
42-
std::string output = ReadAllFromStream(std::shared_ptr<Aws::IOStream>(chunkedStream, [](Aws::IOStream*){}));
43-
auto expectedStreamWithChecksum = "A\r\n1234567890\r\nA\r\n1234567890\r\n5\r\n12345\r\n0\r\nx-amz-checksum-crc32:78DeVw==\r\n\r\n";
44-
EXPECT_EQ(expectedStreamWithChecksum, output);
34+
withChunkedStream("1234567890123456789012345", 10, [](Aws::IOStream& chunkedStream) {
35+
char buffer[100];
36+
std::stringstream output;
37+
38+
// Read in 10-byte chunks like original test
39+
for (int i = 0; i < 4; i++) {
40+
chunkedStream.read(buffer, 10);
41+
auto bytesRead = chunkedStream.gcount();
42+
output.write(buffer, bytesRead);
43+
}
44+
45+
// Read trailing checksum (greater than 10 chars)
46+
chunkedStream.read(buffer, 40);
47+
auto bytesRead = static_cast<size_t>(chunkedStream.gcount());
48+
EXPECT_EQ(36ul, bytesRead);
49+
output.write(buffer, bytesRead);
50+
51+
EXPECT_EQ("A\r\n1234567890\r\nA\r\n1234567890\r\n5\r\n12345\r\n0\r\nx-amz-checksum-crc32:78DeVw==\r\n\r\n", output.str());
52+
});
4553
}
4654

4755
TEST_F(ChunkingInterceptorTest, ShouldNotRequireTwoReadsOnSmallChunk) {
48-
StandardHttpRequest request{"www.clemar.com/strohl", Http::HttpMethod::HTTP_GET};
49-
auto requestHash = Aws::MakeShared<CRC32>(CHUNKING_TEST_LOG_TAG);
50-
request.SetRequestHash("crc32", requestHash);
51-
std::shared_ptr<Aws::IOStream> inputStream = Aws::MakeShared<Aws::StringStream>(CHUNKING_TEST_LOG_TAG, "12345");
52-
53-
AwsChunkedStreamWrapper wrapper{&request, inputStream, 100};
54-
auto chunkedStream = wrapper.GetIOStream();
55-
56-
std::string output = ReadAllFromStream(std::shared_ptr<Aws::IOStream>(chunkedStream, [](Aws::IOStream*){}));
57-
auto expectedStreamWithChecksum = "5\r\n12345\r\n0\r\nx-amz-checksum-crc32:y/U6HA==\r\n\r\n";
58-
EXPECT_EQ(expectedStreamWithChecksum, output);
56+
withChunkedStream("12345", 100, [](Aws::IOStream& chunkedStream) {
57+
char buffer[100];
58+
chunkedStream.read(buffer, 100);
59+
auto bytesRead = static_cast<size_t>(chunkedStream.gcount());
60+
EXPECT_EQ(46ul, bytesRead);
61+
62+
std::string output(buffer, bytesRead);
63+
EXPECT_EQ("5\r\n12345\r\n0\r\nx-amz-checksum-crc32:y/U6HA==\r\n\r\n", output);
64+
});
5965
}
6066

6167
TEST_F(ChunkingInterceptorTest, ShouldWorkOnSmallBuffer) {
62-
StandardHttpRequest request{"www.eugief.com/hesimay", Http::HttpMethod::HTTP_GET};
63-
auto requestHash = Aws::MakeShared<CRC32>(CHUNKING_TEST_LOG_TAG);
64-
request.SetRequestHash("crc32", requestHash);
65-
std::shared_ptr<Aws::IOStream> inputStream = Aws::MakeShared<Aws::StringStream>(CHUNKING_TEST_LOG_TAG, "1234567890");
66-
67-
AwsChunkedStreamWrapper wrapper{&request, inputStream, 5};
68-
auto chunkedStream = wrapper.GetIOStream();
69-
70-
std::string output = ReadAllFromStream(std::shared_ptr<Aws::IOStream>(chunkedStream, [](Aws::IOStream*){}));
71-
auto expectedStreamWithChecksum = "5\r\n12345\r\n5\r\n67890\r\n0\r\nx-amz-checksum-crc32:Jh2u5Q==\r\n\r\n";
72-
EXPECT_EQ(expectedStreamWithChecksum, output);
68+
withChunkedStream("1234567890", 5, [](Aws::IOStream& chunkedStream) {
69+
char buffer[100];
70+
71+
// First read - explicitly ask for 10 bytes (first chunk: "5\r\n12345\r\n")
72+
chunkedStream.read(buffer, 10);
73+
auto bytesRead = static_cast<size_t>(chunkedStream.gcount());
74+
EXPECT_EQ(10ul, bytesRead);
75+
std::string firstRead(buffer, bytesRead);
76+
EXPECT_EQ("5\r\n12345\r\n", firstRead);
77+
78+
// Second read - now we expect the rest (46 bytes: second chunk + trailer)
79+
chunkedStream.read(buffer, 100);
80+
bytesRead = static_cast<size_t>(chunkedStream.gcount());
81+
EXPECT_EQ(46ul, bytesRead);
82+
std::string secondRead(buffer, bytesRead);
83+
EXPECT_EQ("5\r\n67890\r\n0\r\nx-amz-checksum-crc32:Jh2u5Q==\r\n\r\n", secondRead);
84+
85+
// Subsequent reads should return 0
86+
chunkedStream.read(buffer, 100);
87+
bytesRead = static_cast<size_t>(chunkedStream.gcount());
88+
EXPECT_EQ(0ul, bytesRead);
89+
});
7390
}
7491

7592
TEST_F(ChunkingInterceptorTest, ShouldWorkOnEmptyStream) {
76-
StandardHttpRequest request{"www.nidia.com/juna", Http::HttpMethod::HTTP_GET};
77-
auto requestHash = Aws::MakeShared<CRC32>(CHUNKING_TEST_LOG_TAG);
78-
request.SetRequestHash("crc32", requestHash);
79-
std::shared_ptr<Aws::IOStream> inputStream = Aws::MakeShared<Aws::StringStream>(CHUNKING_TEST_LOG_TAG, "");
80-
81-
AwsChunkedStreamWrapper wrapper{&request, inputStream, 5};
82-
auto chunkedStream = wrapper.GetIOStream();
83-
84-
std::string output = ReadAllFromStream(std::shared_ptr<Aws::IOStream>(chunkedStream, [](Aws::IOStream*){}));
85-
auto expectedStreamWithChecksum = "0\r\nx-amz-checksum-crc32:AAAAAA==\r\n\r\n";
86-
EXPECT_EQ(expectedStreamWithChecksum, output);
93+
withChunkedStream("", 5, [](Aws::IOStream& chunkedStream) {
94+
char buffer[100];
95+
chunkedStream.read(buffer, 100);
96+
auto bytesRead = static_cast<size_t>(chunkedStream.gcount());
97+
EXPECT_EQ(36ul, bytesRead);
98+
99+
std::string output(buffer, bytesRead);
100+
EXPECT_EQ("0\r\nx-amz-checksum-crc32:AAAAAA==\r\n\r\n", output);
101+
});
87102
}

0 commit comments

Comments
 (0)