Skip to content

Commit 8ff1bec

Browse files
committed
add configuration to preserve path seperator in URIs
1 parent 832ce62 commit 8ff1bec

File tree

7 files changed

+151
-6
lines changed

7 files changed

+151
-6
lines changed

src/aws-cpp-sdk-core/include/aws/core/Aws.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ namespace Aws
105105
* Disable legacy URL encoding that leaves `$&,:@=` unescaped for legacy purposes.
106106
*/
107107
bool compliantRfc3986Encoding;
108+
/**
109+
* When constructing Path segments in a URI preserve path separators instead of collapsing
110+
* slashes. This is useful for aligning with other SDKs and tools on key path for S3 objects
111+
* as currently the C++ SDK sanitizes the path.
112+
*/
113+
bool preservePathSeparators = false;
108114
};
109115

110116
/**

src/aws-cpp-sdk-core/include/aws/core/http/URI.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ namespace Aws
2424
extern bool s_compliantRfc3986Encoding;
2525
AWS_CORE_API void SetCompliantRfc3986Encoding(bool compliant);
2626

27+
extern bool s_preservePathSeparators;
28+
AWS_CORE_API void SetPreservePathSeparators(bool preservePathSeparators);
29+
2730
//per https://tools.ietf.org/html/rfc3986#section-3.4 there is nothing preventing servers from allowing
2831
//multiple values for the same key. So use a multimap instead of a map.
2932
typedef Aws::MultiMap<Aws::String, Aws::String> QueryStringParameterCollection;
@@ -135,7 +138,10 @@ namespace Aws
135138
Aws::StringStream ss;
136139
ss << pathSegments;
137140
Aws::String segments = ss.str();
138-
for (const auto& segment : Aws::Utils::StringUtils::Split(segments, '/'))
141+
const auto splitOption = s_preservePathSeparators
142+
? Utils::StringUtils::SplitOptions::PRESERVE_DELIMITERS
143+
: Utils::StringUtils::SplitOptions::NOT_SET;
144+
for (const auto& segment : Aws::Utils::StringUtils::Split(segments, '/', splitOption))
139145
{
140146
m_pathSegments.push_back(segment);
141147
}

src/aws-cpp-sdk-core/include/aws/core/utils/StringUtils.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ namespace Aws
8181
/**
8282
* Includes empty entries in the vector returned by Split()
8383
*/
84-
INCLUDE_EMPTY_ENTRIES
84+
INCLUDE_EMPTY_ENTRIES,
85+
/**
86+
* Include delimeter in vector returned, however removing leading and trailing
87+
* delimiters.
88+
*/
89+
PRESERVE_DELIMITERS,
8590
};
8691

8792
/**
@@ -116,6 +121,14 @@ namespace Aws
116121
*/
117122
static Aws::Vector<Aws::String> Split(const Aws::String& toSplit, char splitOn, size_t numOfTargetParts, SplitOptions option);
118123

124+
/**
125+
* Splits a string on delimeter, keeping the delimeter in the vector returned.
126+
* @param toSplit, the original string to split
127+
* @param splitOn, the delimiter you want to use.
128+
* @param numOfTargetParts, how many target parts you want to get, if it is 0, as many as possible.
129+
*/
130+
static Aws::Vector<Aws::String> SplitWithDelimiters(const Aws::String& toSplit, char splitOn, size_t numOfTargetParts);
131+
119132
/**
120133
* Splits a string on new line characters.
121134
*/

src/aws-cpp-sdk-core/source/Aws.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ namespace Aws
155155
Aws::Http::SetInitCleanupCurlFlag(options.httpOptions.initAndCleanupCurl);
156156
Aws::Http::SetInstallSigPipeHandlerFlag(options.httpOptions.installSigPipeHandler);
157157
Aws::Http::SetCompliantRfc3986Encoding(options.httpOptions.compliantRfc3986Encoding);
158+
Aws::Http::SetPreservePathSeparators(options.httpOptions.preservePathSeparators);
158159
Aws::Http::InitHttp();
159160
Aws::InitializeEnumOverflowContainer();
160161
cJSON_AS4CPP_Hooks hooks;

src/aws-cpp-sdk-core/source/http/URI.cpp

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const char* SEPARATOR = "://";
2727
bool s_compliantRfc3986Encoding = false;
2828
void SetCompliantRfc3986Encoding(bool compliant) { s_compliantRfc3986Encoding = compliant; }
2929

30+
bool s_preservePathSeparators = false;
31+
void SetPreservePathSeparators(bool preservePathSeparators) { s_preservePathSeparators = preservePathSeparators; }
32+
3033
Aws::String urlEncodeSegment(const Aws::String& segment, bool rfcEncoded = false)
3134
{
3235
// consolidates legacy escaping logic into one local method
@@ -199,8 +202,18 @@ Aws::String URI::GetPath() const
199202

200203
for (auto const& segment : m_pathSegments)
201204
{
202-
path.push_back('/');
203-
path.append(segment);
205+
if((s_preservePathSeparators && segment == "/"))
206+
{
207+
path.push_back('/');
208+
}
209+
else
210+
{
211+
if (!s_preservePathSeparators)
212+
{
213+
path.push_back('/');
214+
}
215+
path.append(segment);
216+
}
204217
}
205218

206219
if (m_pathSegments.empty() || m_pathHasTrailingSlash)
@@ -217,7 +230,18 @@ Aws::String URI::GetURLEncodedPath() const
217230

218231
for (auto const& segment : m_pathSegments)
219232
{
220-
ss << '/' << StringUtils::URLEncode(segment.c_str());
233+
if((s_preservePathSeparators && segment == "/"))
234+
{
235+
ss << "/";
236+
}
237+
else
238+
{
239+
if (!s_preservePathSeparators)
240+
{
241+
ss << "/";
242+
}
243+
ss << StringUtils::URLEncode(segment.c_str());
244+
}
221245
}
222246

223247
if (m_pathSegments.empty() || m_pathHasTrailingSlash)
@@ -237,7 +261,18 @@ Aws::String URI::GetURLEncodedPathRFC3986() const
237261
// (mostly; there is some non-standards legacy support that can be disabled)
238262
for (const auto& segment : m_pathSegments)
239263
{
240-
ss << '/' << urlEncodeSegment(segment, m_useRfcEncoding);
264+
if((s_preservePathSeparators && segment == "/"))
265+
{
266+
ss << "/";
267+
}
268+
else
269+
{
270+
if (!s_preservePathSeparators)
271+
{
272+
ss << "/";
273+
}
274+
ss << urlEncodeSegment(segment, m_useRfcEncoding);
275+
}
241276
}
242277

243278
if (m_pathSegments.empty() || m_pathHasTrailingSlash)

src/aws-cpp-sdk-core/source/utils/StringUtils.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ Aws::Vector<Aws::String> StringUtils::Split(const Aws::String& toSplit, char spl
9090

9191
Aws::Vector<Aws::String> StringUtils::Split(const Aws::String& toSplit, char splitOn, size_t numOfTargetParts, SplitOptions option)
9292
{
93+
if (option == SplitOptions::PRESERVE_DELIMITERS)
94+
{
95+
return StringUtils::SplitWithDelimiters(toSplit, splitOn, numOfTargetParts);
96+
}
97+
9398
Aws::Vector<Aws::String> returnValues;
9499
Aws::StringStream input(toSplit);
95100
Aws::String item;
@@ -128,6 +133,28 @@ Aws::Vector<Aws::String> StringUtils::Split(const Aws::String& toSplit, char spl
128133
return returnValues;
129134
}
130135

136+
Aws::Vector<Aws::String> StringUtils::SplitWithDelimiters(const Aws::String& toSplit, char splitOn, size_t numOfTargetParts)
137+
{
138+
Aws::Vector<Aws::String> returnValues;
139+
size_t start = 0;
140+
size_t found = toSplit.find_first_of(splitOn, start);
141+
142+
while (returnValues.size() < numOfTargetParts && found != std::string::npos) {
143+
if (found > start) {
144+
returnValues.push_back(toSplit.substr(start, found - start));
145+
}
146+
returnValues.push_back(toSplit.substr(found, 1));
147+
start = found + 1;
148+
found = toSplit.find_first_of(splitOn, start);
149+
}
150+
151+
if (start < toSplit.length()) {
152+
returnValues.push_back(toSplit.substr(start));
153+
}
154+
155+
return returnValues;
156+
}
157+
131158
Aws::Vector<Aws::String> StringUtils::SplitOnLine(const Aws::String& toSplit)
132159
{
133160
Aws::StringStream input(toSplit);

tests/aws-cpp-sdk-s3-unit-tests/S3UnitTests.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,60 @@ TEST_F(S3UnitTest, S3UriMiddleDots) {
124124
const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
125125
EXPECT_EQ("https://bluerev.s3.us-east-1.amazonaws.com/belinda/../says", seenRequest.GetUri().GetURIString());
126126
}
127+
128+
TEST_F(S3UnitTest, S3UriPathPreservationOff) {
129+
auto putObjectRequest = PutObjectRequest()
130+
.WithBucket("vu")
131+
.WithKey("////stephanie////says////////////that////////she//wants///////to/know.txt");
132+
133+
std::shared_ptr<IOStream> body = Aws::MakeShared<StringStream>(ALLOCATION_TAG,
134+
"What country shall I say is calling From across the world?",
135+
std::ios_base::in | std::ios_base::binary);
136+
137+
putObjectRequest.SetBody(body);
138+
139+
//We have to mock requset because it is used to create the return body, it actually isnt used.
140+
auto mockRequest = Aws::MakeShared<Standard::StandardHttpRequest>(ALLOCATION_TAG, "mockuri", HttpMethod::HTTP_GET);
141+
mockRequest->SetResponseStreamFactory([]() -> IOStream* {
142+
return Aws::New<StringStream>(ALLOCATION_TAG, "response-string", std::ios_base::in | std::ios_base::binary);
143+
});
144+
auto mockResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, mockRequest);
145+
mockResponse->SetResponseCode(HttpResponseCode::OK);
146+
_mockHttpClient->AddResponseToReturn(mockResponse);
147+
148+
const auto response = _s3Client->PutObject(putObjectRequest);
149+
AWS_EXPECT_SUCCESS(response);
150+
151+
const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
152+
EXPECT_EQ("https://s3.us-east-1.amazonaws.com/vu/stephanie/says/that/she/wants/to/know.txt", seenRequest.GetUri().GetURIString());
153+
}
154+
155+
TEST_F(S3UnitTest, S3UriPathPreservationOn) {
156+
//Turn on path preservation
157+
Aws::Http::SetPreservePathSeparators(true);
158+
159+
auto putObjectRequest = PutObjectRequest()
160+
.WithBucket("vu")
161+
.WithKey("////stephanie////says////////////that////////she//wants///////to/know.txt");
162+
163+
std::shared_ptr<IOStream> body = Aws::MakeShared<StringStream>(ALLOCATION_TAG,
164+
"What country shall I say is calling From across the world?",
165+
std::ios_base::in | std::ios_base::binary);
166+
167+
putObjectRequest.SetBody(body);
168+
169+
//We have to mock requset because it is used to create the return body, it actually isnt used.
170+
auto mockRequest = Aws::MakeShared<Standard::StandardHttpRequest>(ALLOCATION_TAG, "mockuri", HttpMethod::HTTP_GET);
171+
mockRequest->SetResponseStreamFactory([]() -> IOStream* {
172+
return Aws::New<StringStream>(ALLOCATION_TAG, "response-string", std::ios_base::in | std::ios_base::binary);
173+
});
174+
auto mockResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, mockRequest);
175+
mockResponse->SetResponseCode(HttpResponseCode::OK);
176+
_mockHttpClient->AddResponseToReturn(mockResponse);
177+
178+
const auto response = _s3Client->PutObject(putObjectRequest);
179+
AWS_EXPECT_SUCCESS(response);
180+
181+
const auto seenRequest = _mockHttpClient->GetMostRecentHttpRequest();
182+
EXPECT_EQ("https://s3.us-east-1.amazonaws.com/vu////stephanie////says////////////that////////she//wants///////to/know.txt", seenRequest.GetUri().GetURIString());
183+
}

0 commit comments

Comments
 (0)