Skip to content

Commit 1a0db27

Browse files
committed
add configuration to preserve path seperator in URIs
1 parent 47db59d commit 1a0db27

File tree

7 files changed

+115
-3
lines changed

7 files changed

+115
-3
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ 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+
* TODO: In the next major release, this will become the default to align better with other SDKs.
114+
*/
115+
bool preservePathSeparators = false;
108116
};
109117

110118
/**

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

Lines changed: 13 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 AWS_CORE_API 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,16 @@ 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::INCLUDE_EMPTY_SEGMENTS
143+
: Utils::StringUtils::SplitOptions::NOT_SET;
144+
// Preserve legacy behavior -- we need to remove a leading "/" if use `INCLUDE_EMPTY_SEGMENTS` is specified
145+
// because string split will no longer ignore leading delimiters -- which is correct.
146+
auto split = Aws::Utils::StringUtils::Split(segments, '/', splitOption);
147+
if (s_preservePathSeparators && m_pathSegments.empty() && !split.empty() && split.front().empty() && !m_pathHasTrailingSlash) {
148+
split.erase(split.begin());
149+
}
150+
for (const auto& segment: split)
139151
{
140152
m_pathSegments.push_back(segment);
141153
}

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,13 @@ namespace Aws
7979
*/
8080
NOT_SET,
8181
/**
82-
* Includes empty entries in the vector returned by Split()
82+
* Deprecated use INCLUDE_EMPTY_SEGMENTS instead.
8383
*/
84-
INCLUDE_EMPTY_ENTRIES
84+
INCLUDE_EMPTY_ENTRIES,
85+
/**
86+
* Include delimiters as empty segments in the split string
87+
*/
88+
INCLUDE_EMPTY_SEGMENTS,
8589
};
8690

8791
/**
@@ -116,6 +120,13 @@ namespace Aws
116120
*/
117121
static Aws::Vector<Aws::String> Split(const Aws::String& toSplit, char splitOn, size_t numOfTargetParts, SplitOptions option);
118122

123+
/**
124+
* Splits a string on delimeter, keeping the delimiter in the string as a empty space.
125+
* @param toSplit, the original string to split
126+
* @param splitOn, the delimiter you want to use.
127+
*/
128+
static Aws::Vector<Aws::String> SplitWithSpaces(const Aws::String& toSplit, char splitOn);
129+
119130
/**
120131
* Splits a string on new line characters.
121132
*/

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: 3 additions & 0 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

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

Lines changed: 20 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::INCLUDE_EMPTY_SEGMENTS)
94+
{
95+
return StringUtils::SplitWithSpaces(toSplit, splitOn);
96+
}
97+
9398
Aws::Vector<Aws::String> returnValues;
9499
Aws::StringStream input(toSplit);
95100
Aws::String item;
@@ -128,6 +133,21 @@ Aws::Vector<Aws::String> StringUtils::Split(const Aws::String& toSplit, char spl
128133
return returnValues;
129134
}
130135

136+
Aws::Vector<Aws::String> StringUtils::SplitWithSpaces(const Aws::String& toSplit, char splitOn)
137+
{
138+
size_t pos = 0;
139+
String split{toSplit};
140+
Vector<String> returnValues;
141+
while ((pos = split.find(splitOn)) != std::string::npos) {
142+
returnValues.emplace_back(split.substr(0, pos));
143+
split.erase(0, pos + 1);
144+
}
145+
if (!split.empty()) {
146+
returnValues.emplace_back(split);
147+
}
148+
return returnValues;
149+
}
150+
131151
Aws::Vector<Aws::String> StringUtils::SplitOnLine(const Aws::String& toSplit)
132152
{
133153
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("velvetunderground")
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://velvetunderground.s3.us-east-1.amazonaws.com/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("velvetunderground")
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://velvetunderground.s3.us-east-1.amazonaws.com/////stephanie////says////////////that////////she//wants///////to/know.txt", seenRequest.GetUri().GetURIString());
183+
}

0 commit comments

Comments
 (0)