Skip to content

Commit 52a6a5d

Browse files
committed
feat(net-http): StringContent takes an Encoding and serialises through it (#2070)
Measured on the old code: StringContent("\xc3\xa9", "utf-16", "text/plain") announced `charset=utf-16` and emitted the two UTF-8 bytes c3 a9. A conforming server reads two octets under a utf-16 label as ONE UTF-16 code unit and gets U+A9C3 -- a different character, silently, with no diagnostic anywhere. The charset was a label; the bytes were always the string's storage bytes. .NET DOES NOT VALIDATE AGAINST THAT. It makes the contradiction UNREPRESENTABLE: its constructor takes an Encoding, serialises through it (GetContentByteArray, StringContent.cs:90-98) and labels the header with that same object's WebName (:73). One object is both the serialiser and the label, so there is no second source of truth for them to disagree about. That is why the repair is a SIGNATURE change and not a check. A check would still let a caller name a charset the body was not encoded in, and would only refuse the charsets this port happened to recognise. The second parameter is now a std::shared_ptr<System::Text::Encoding>, and nullptr means UTF-8 -- .NET's `encoding ??= DefaultStringEncoding` at :53. StringContent("e") unchanged: c3 a9, charset=utf-8 StringContent("e", "utf-16") no longer compiles StringContent("e", Encoding::Unicode()) e9 00, charset=utf-16 StringContent("e", nullptr, "text/plain") UTF-8 The web names are .NET's and were verified by probe before the change rather than assumed: utf-8, utf-16, us-ascii, iso-8859-1, utf-32. The probe binary was deleted afterwards, per the build-resource policy. A PUBLIC SOURCE BREAK, landed under SA-2 with all five conditions discharged: 1. migration note -- docs/Migration-StringContentEncoding.md 2. negative consumer fixture -- test/consumer/net_http_stringcontent_encoding_negative.cpp, three sites, including the two-argument StringContent(body, "utf-8") that is most likely to survive a careless migration. Fixture set grows to 36 fixtures / 197 sites. 3. downstream ticket #2379 4. the full gate 5. measured impact -- neither cna nor mobile-eggbert references StringContent at all, ZERO sites in both. Neither was modified. ONE CHECK BECAME UNNECESSARY AND IS KEPT ANYWAY. #2063 rejects a CR/LF/NUL in the charset because it is concatenated into a Content-Type field. The charset can no longer carry one, since it now comes from an Encoding's own web name -- the state is unrepresentable rather than rejected, which is the stronger of the two. The check stays because it costs nothing and a future encoding is not obliged to have a well-formed name. A NEW COMPONENT EDGE. Net.Http now depends on Text: the graph goes 41 modules / 92 edges to 41 / 93, and docs/ComponentCatalog.md is regenerated. There is no alternative that keeps the count -- the whole point is to encode through System::Text::Encoding, and a private copy of even one encoder would be the duplication #2354 has just finished removing six of. One gated pin inverted, two cases added. Three mutations, all caught: store the raw string instead of encoding it; label the header utf-8 regardless of the encoding; a null encoding means UTF-16 rather than UTF-8. Gate: 17,320 run, 17,320 passed, 0 failed, 0 skipped across 38 executables, GREEN. Module boundaries valid (41 modules, 93 edges). Negative fixtures OK (36 fixtures, 197 sites).
1 parent 933fde2 commit 52a6a5d

8 files changed

Lines changed: 297 additions & 46 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/ComponentCatalog.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file is generated from the CMake component registrations. Run
88
`python3 scripts/generate_component_catalog.py` after changing module
99
metadata, or use `--check` to verify that the committed catalogue is current.
1010

11-
The graph contains **41 physical modules** and **92 direct production dependency edges**.
11+
The graph contains **41 physical modules** and **93 direct production dependency edges**.
1212

1313
For each row, the component name and representative public header form a
1414
minimal consumer example using the template below:
@@ -57,7 +57,7 @@ for maintainers and are not part of the consumer include surface.
5757
| `IO.IsolatedStorage` | `modules/io-isolated-storage` | static | `Core.Base`, `IO` | `Storage` ||| `System/IO/IsolatedStorage/IsolatedStorage.hpp` |
5858
| `Net` | `modules/net` | static | `Collections.Core`, `ComponentModel`, `Core.Base`, `Uri` ||| `ws2_32` on Windows (private) | `System/Net/Cookie.hpp` |
5959
| `Net.Sockets` | `modules/net-sockets` | static | `Core.Base`, `IO`, `Net`, `Threading.Tasks` |||| `System/Net/Sockets/IPPacketInformation.hpp` |
60-
| `Net.Http` | `modules/net-http` | static | `Core.Base`, `IO`, `Net`, `Threading`, `Threading.Tasks` | `Uri` | `Net.Sockets` || `System/Net/Http/ByteArrayContent.hpp` |
60+
| `Net.Http` | `modules/net-http` | static | `Core.Base`, `IO`, `Net`, `Text`, `Threading`, `Threading.Tasks` | `Uri` | `Net.Sockets` || `System/Net/Http/ByteArrayContent.hpp` |
6161
| `Net.Http.Headers` | `modules/net-http-headers` | static | `Collections.Core`, `Core.Base`, `Uri` | `Net` ||| `System/Net/Http/Headers/AuthenticationHeaderValue.hpp` |
6262
| `Net.Http.Json` | `modules/net-http-json` | interface | `Core.Base`, `Net.Http`, `Text.Json`, `Threading.Tasks` || `Net.Sockets` || `System/Net/Http/Json/HttpClientJsonExtensions.hpp` |
6363
| `Net.Mime` | `modules/net-mime` | static | `Collections.Core`, `Core.Base` |||| `System/Net/Mime/ContentType.hpp` |
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
<!-- Copyright (c) Robert Vokac and contributors -->
3+
4+
# Migration — `StringContent` takes an `Encoding`, not a charset string (ticket #2070)
5+
6+
*2026-08-18.* `System::Net::Http::StringContent`'s second parameter changed from a charset
7+
**string** to a `std::shared_ptr<System::Text::Encoding>`, and the body is now **serialised
8+
through** that encoding.
9+
10+
**This is a public source break.** Landed under `docs/StandingApprovals.md` SA-2, with all five
11+
conditions discharged — §7. No layout, vtable or `noexcept` concern; the type is header-only.
12+
13+
---
14+
15+
## 1. What was wrong
16+
17+
The charset was a **label only**. The bytes emitted were always the string's UTF-8 storage bytes,
18+
so the declared charset and the payload could contradict each other:
19+
20+
```cpp
21+
StringContent body("\xc3\xa9", "utf-16", "text/plain");
22+
// Content-Type: text/plain; charset=utf-16
23+
// body bytes: c3 a9
24+
```
25+
26+
`c3 a9` is `é` in UTF-8. A conforming server reads two octets under a `utf-16` label as **one**
27+
UTF-16 code unit and gets `U+A9C3` — a different character, silently, with no diagnostic anywhere.
28+
29+
## 2. What .NET does, and why it is a different shape
30+
31+
.NET does not validate against the contradiction. It makes the contradiction **unrepresentable**:
32+
33+
```csharp
34+
public StringContent(string content, Encoding? encoding, string? mediaType)
35+
: base(GetContentByteArray(content, encoding)) // serialise through it
36+
{
37+
encoding ??= DefaultStringEncoding; // null means UTF-8
38+
39+
Headers.ContentType = new MediaTypeHeaderValue(mediaType, encoding.WebName); // label from it
40+
}
41+
```
42+
*(`StringContent.cs:48-73`; `GetContentByteArray` is `:90-98`.)*
43+
44+
One `Encoding` object is both the serialiser and the label, so there is no second source of truth
45+
for them to disagree about. That is why the repair is a **signature** change and not a validation
46+
check: a check would still let a caller name a charset the body was not encoded in, and would
47+
merely refuse the ones the port happened to recognise.
48+
49+
## 3. What changed
50+
51+
| Call | Was | Is |
52+
|---|---|---|
53+
| `StringContent("é")` | `c3 a9`, `charset=utf-8` | **unchanged** |
54+
| `StringContent("é", "utf-8", …)` | compiled | **does not compile** — pass `Encoding::UTF8()` |
55+
| `StringContent("é", "utf-16")` | `c3 a9` under a `utf-16` label | does not compile |
56+
| `StringContent("é", Encoding::Unicode())` || `e9 00`, `charset=utf-16` |
57+
| `StringContent("é", nullptr, …)` || UTF-8, matching `encoding ??= DefaultStringEncoding` |
58+
| `getCharSetProperty()` | whatever the caller said | the encoding's own `WebName`, always |
59+
60+
`ReadAsString()` returns the **encoded** bytes as a `std::string`. Under a non-UTF-8 encoding
61+
those are not UTF-8 storage bytes and must not be treated as text; `ReadAsByteArray()` is the
62+
honest accessor for a non-UTF-8 body.
63+
64+
## 4. To migrate
65+
66+
```cpp
67+
StringContent body(text, "utf-16", "text/plain"); // before
68+
StringContent body(text, System::Text::Encoding::Unicode(), "text/plain"); // after
69+
70+
StringContent json(text, "utf-8", "application/json"); // before
71+
StringContent json(text, System::Text::Encoding::UTF8(), "application/json"); // after
72+
73+
StringContent plain(text); // unchanged — still UTF-8, still text/plain
74+
```
75+
76+
The web names are .NET's and were verified by probe: `utf-8`, `utf-16`, `us-ascii`,
77+
`iso-8859-1`, `utf-32`.
78+
79+
## 5. One check became unnecessary and is kept anyway
80+
81+
#2063 rejects a CR/LF/NUL in the charset, because it is concatenated into a `Content-Type` field.
82+
The charset can no longer carry one, since it now comes from an `Encoding`'s own web name — the
83+
state is unrepresentable rather than rejected, which is the stronger of the two. The check is kept
84+
because it costs nothing and a future encoding is not obliged to have a well-formed name.
85+
86+
## 6. A new component edge
87+
88+
`Net.Http` now depends on `Text`. The module graph goes **41 modules / 92 edges → 41 / 93**, and
89+
`docs/ComponentCatalog.md` is regenerated. There is no alternative that keeps the edge count: the
90+
whole point is to encode through `System::Text::Encoding`, and a private copy of even one encoder
91+
would be the duplication #2354 has just finished removing six of.
92+
93+
## 7. SA-2's five conditions
94+
95+
1. **Migration note** — this document.
96+
2. **Negative consumer fixture** — `test/consumer/net_http_stringcontent_encoding_negative.cpp`,
97+
three sites, including the two-argument `StringContent(body, "utf-8")` most likely to survive a
98+
careless migration. The fixture set grows to **36 fixtures / 197 sites**.
99+
3. **Downstream ticket** — #2379.
100+
4. **Full gate** — 17,320 run, 17,320 passed, 0 failed.
101+
5. **Measured impact** — neither `cna` nor `mobile-eggbert` references `StringContent` at all:
102+
**zero sites in both**. Neither repository was modified.
103+
104+
## 8. Evidence
105+
106+
| Mutation | Caught |
107+
|---|---|
108+
| Store the raw string instead of encoding it (the pre-#2070 behaviour) | ✅ (2 tests) |
109+
| Label the header `utf-8` regardless of the encoding | ✅ (2 tests) |
110+
| A null encoding means UTF-16 rather than UTF-8 | ✅ (3 tests) |

modules/net-http/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ sharp_runtime_register_module(
55
NAME Net.Http
66
TARGET sharp_runtime_net_http
77
TYPE STATIC
8-
PUBLIC_DEPENDENCIES Core.Base IO Net Threading Threading.Tasks
8+
PUBLIC_DEPENDENCIES Core.Base IO Net Text Threading Threading.Tasks
99
PRIVATE_DEPENDENCIES Uri
1010
TEST_DEPENDENCIES Net.Sockets
1111
)

modules/net-http/include/System/Net/Http/StringContent.hpp

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,57 +4,80 @@
44
#pragma once
55
#include "System/Net/Http/HttpContent.hpp"
66
#include "System/Net/Http/detail/HttpFieldValidation.hpp"
7+
#include "System/Text/Encoding.hpp"
8+
#include <memory>
79
#include <string>
810
#include <vector>
911

1012
namespace System::Net::Http {
1113

1214
/** HTTP content backed by a plain text string, mirroring .NET System.Net.Http.StringContent. */
1315
class StringContent : public HttpContent {
14-
std::string content_;
15-
std::string mediaType_;
16-
std::string charset_;
16+
std::vector<SharpRuntime::bytecs> bytes_;
17+
std::string mediaType_;
18+
std::string charset_;
1719
public:
1820
/**
1921
* @brief Constructs StringContent from a plain text string.
2022
* @param content The text body.
21-
* @param charset Character encoding; defaults to "utf-8".
22-
* @param mediaType MIME type; defaults to "text/plain".
23+
* @param encoding The encoding to serialise @p content through; `nullptr` means UTF-8.
24+
* @param mediaType MIME type; defaults to `"text/plain"`.
2325
*
24-
* @throws System::FormatException if @p charset or @p mediaType contains a carriage
25-
* return, a line feed or a NUL character.
26+
* @throws System::FormatException if @p mediaType or the encoding's web name contains a
27+
* carriage return, a line feed or a NUL character.
2628
*
27-
* @note **Narrowing since ticket #2063** (SR-AUD-313, cause NH-B). Both values are
28-
* concatenated into a `Content-Type: <mediaType>; charset=<charset>` field — by
29+
* @note **Ticket #2070 (2026-08-18) changed this parameter from a charset STRING to an
30+
* `Encoding`, which is a public source break.** The old signature let the declared charset
31+
* and the emitted bytes contradict each other: `StringContent("\xc3\xa9", "utf-16")`
32+
* labelled its body `charset=utf-16` and emitted the two UTF-8 bytes `c3 a9`, so a
33+
* conforming server decoded them as a single UTF-16 code unit and got `U+A9C3` — a wrong
34+
* character, silently, with no diagnostic anywhere.
35+
*
36+
* .NET makes that state **unrepresentable** rather than validating against it. Its
37+
* constructor takes an `Encoding`, serialises through it
38+
* (`GetContentByteArray`, StringContent.cs:90-98) and then labels the header with that same
39+
* object's `WebName` (`:73`). One source of truth, so the two cannot disagree. `null` means
40+
* UTF-8, which is `encoding ??= DefaultStringEncoding` at `:53`.
41+
*
42+
* @note **Narrowing since ticket #2063** (SR-AUD-313, cause NH-B). The media type and the
43+
* charset are concatenated into a `Content-Type: <mediaType>; charset=<charset>` field — by
2944
* `HttpClientHandler::Send` on the wire and by `MultipartContent::ReadAsString` inside a
3045
* MIME part — so a CR/LF in either used to emit extra header fields. The **body** is not
31-
* validated: it is payload, not a protocol field.
32-
*
33-
* @note The charset is a **label only**: the bytes emitted are always the storage bytes of
34-
* @p content, so `StringContent("\xc3\xa9", "utf-16")` still emits `c3 a9`. Whether .NET
35-
* encodes through the declared charset or treats the label as advisory is an open
36-
* question with no repository-contained evidence — deferred ticket #2070, with the
37-
* current behaviour pinned so no answer can land silently.
46+
* validated: it is payload, not a protocol field. The charset can no longer carry one at
47+
* all, since it now comes from an `Encoding`'s own web name, but the check is kept: it
48+
* costs nothing and a future encoding is not required to have a well-formed name.
3849
*/
39-
explicit StringContent(const std::string& content,
40-
const std::string& charset = "utf-8",
41-
const std::string& mediaType = "text/plain")
42-
: content_(content), mediaType_(mediaType), charset_(charset) {
43-
detail::ThrowIfControlCharacter(charset, "charset");
50+
explicit StringContent(const std::string& content,
51+
const std::shared_ptr<System::Text::Encoding>& encoding = nullptr,
52+
const std::string& mediaType = "text/plain")
53+
: mediaType_(mediaType) {
54+
const auto& effective = encoding ? encoding : System::Text::Encoding::UTF8();
55+
charset_ = effective->getWebNameProperty();
56+
detail::ThrowIfControlCharacter(charset_, "charset");
4457
detail::ThrowIfControlCharacter(mediaType, "media type");
58+
bytes_ = effective->GetBytes(content);
4559
}
4660

47-
/** Returns the content body as a string. */
48-
[[nodiscard]] std::string ReadAsString() const override { return content_; }
61+
/**
62+
* @brief Returns the encoded body as a string of raw bytes.
63+
*
64+
* Under a non-UTF-8 encoding these are **not** UTF-8 storage bytes and must not be treated
65+
* as text: `ReadAsString()` on a UTF-16 body returns the UTF-16 octets. That is the same
66+
* thing .NET's `ByteArrayContent.ReadAsStringAsync` would do without a charset to decode by,
67+
* and it is why `ReadAsByteArray()` is the honest accessor for a non-UTF-8 body.
68+
*/
69+
[[nodiscard]] std::string ReadAsString() const override {
70+
return std::string(bytes_.begin(), bytes_.end());
71+
}
4972

50-
/** Returns the content body as a raw byte array (UTF-8 encoding assumed). */
73+
/** Returns the content body as the raw bytes of the declared charset. */
5174
[[nodiscard]] std::vector<SharpRuntime::bytecs> ReadAsByteArray() const override {
52-
return std::vector<SharpRuntime::bytecs>(content_.begin(), content_.end());
75+
return bytes_;
5376
}
5477

5578
/** Returns the MIME type of the content (e.g. "text/plain"). */
5679
[[nodiscard]] std::string getContentTypeProperty() const override { return mediaType_; }
57-
/** Returns the character set of the content (e.g. "utf-8"). */
80+
/** Returns the character set of the content — the encoding's own web name, always. */
5881
[[nodiscard]] std::string getCharSetProperty() const override { return charset_; }
5982
};
6083

modules/net-http/tests/System/Net/HttpClientTests.cpp

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "System/Net/Http/HttpMethod.hpp"
1919
#include "System/Net/Http/HttpContent.hpp"
2020
#include "System/Net/Http/StringContent.hpp"
21+
#include "System/Text/Encoding.hpp"
2122
#include "System/Net/Http/ByteArrayContent.hpp"
2223
#include "System/Net/Http/HttpRequestMessage.hpp"
2324
#include "System/Net/Http/HttpResponseMessage.hpp"
@@ -147,7 +148,7 @@ TEST(StringContentTests, DefaultContentType) {
147148
}
148149

149150
TEST(StringContentTests, CustomMediaType) {
150-
StringContent c("{}", "utf-8", "application/json");
151+
StringContent c("{}", System::Text::Encoding::UTF8(), "application/json");
151152
EXPECT_EQ(c.getContentTypeProperty(), "application/json");
152153
EXPECT_EQ(c.getCharSetProperty(), "utf-8");
153154
}
@@ -707,9 +708,9 @@ TEST(MultipartContentTests, ReadAsString_EmptyContent_JustBoundaries) {
707708

708709
TEST(MultipartContentTests, ReadAsString_ThreeParts_ExactByteLayout) {
709710
MultipartContent c("mixed", "B");
710-
c.Add(std::make_shared<StringContent>("one", "", ""));
711-
c.Add(std::make_shared<StringContent>("two", "", ""));
712-
c.Add(std::make_shared<StringContent>("three", "", ""));
711+
c.Add(std::make_shared<StringContent>("one", nullptr, ""));
712+
c.Add(std::make_shared<StringContent>("two", nullptr, ""));
713+
c.Add(std::make_shared<StringContent>("three", nullptr, ""));
713714
std::string body = c.ReadAsString();
714715

715716
EXPECT_EQ(body,
@@ -1712,8 +1713,11 @@ TEST(HttpControlCharacterTests, ParseStatusLine_ControlCharacter_ThrowsHttpReque
17121713

17131714
TEST(HttpControlCharacterTests, ContentMediaTypeAndCharset_ControlCharacter_ThrowsFormatException) {
17141715
for (const auto& bad : controlBearingFields()) {
1715-
EXPECT_THROW(StringContent("body", "utf-8", bad), System::FormatException);
1716-
EXPECT_THROW(StringContent("body", bad, "text/plain"), System::FormatException);
1716+
EXPECT_THROW(StringContent("body", System::Text::Encoding::UTF8(), bad),
1717+
System::FormatException);
1718+
// #2070: the CHARSET can no longer carry a control character, because it is no longer a
1719+
// caller-supplied string -- it is the Encoding's own web name. The state is now
1720+
// unrepresentable rather than rejected, which is the stronger of the two.
17171721
EXPECT_THROW(ByteArrayContent(std::vector<SharpRuntime::bytecs>{1, 2}, bad),
17181722
System::FormatException);
17191723

@@ -1728,7 +1732,7 @@ TEST(HttpControlCharacterTests, ContentMediaTypeAndCharset_ControlCharacter_Thro
17281732

17291733
// The BODY is payload, not a protocol field, and must not be validated.
17301734
TEST(HttpControlCharacterTests, ContentBodyWithControlCharacters_StillAccepted) {
1731-
StringContent content("line1\r\nline2\r\n", "utf-8", "text/plain");
1735+
StringContent content("line1\r\nline2\r\n", System::Text::Encoding::UTF8(), "text/plain");
17321736
EXPECT_EQ(content.ReadAsString(), "line1\r\nline2\r\n");
17331737
ByteArrayContent bytes(std::vector<SharpRuntime::bytecs>{'\r', '\n', 0, 'x'});
17341738
EXPECT_EQ(bytes.ReadAsByteArray().size(), 4u);
@@ -2097,15 +2101,53 @@ TEST(NetHttpGatedBehaviourPins, Fix2069_TheStatusCodeDomainIsZeroToNineHundredNi
20972101
EXPECT_EQ(response.getStatusCodeProperty(), HttpStatusCode::OK);
20982102
}
20992103

2100-
// #2070 (SR-AUD-317) -- the charset is a label; the bytes are always the
2101-
// storage bytes. Whether .NET encodes through the label is unverified.
2102-
TEST(NetHttpGatedBehaviourPins, Pin2070_StringContentEmitsStorageBytesUnderAnyCharsetLabel) {
2103-
StringContent content("\xc3\xa9", "utf-16", "text/plain");
2104-
auto bytes = content.ReadAsByteArray();
2105-
ASSERT_EQ(bytes.size(), 2u);
2106-
EXPECT_EQ(static_cast<unsigned>(bytes[0]), 0xc3u);
2107-
EXPECT_EQ(static_cast<unsigned>(bytes[1]), 0xa9u);
2108-
EXPECT_EQ(content.getCharSetProperty(), "utf-16");
2104+
// #2070 (SR-AUD-317) RESOLVED, and the pin is inverted. The charset used to be a LABEL: the
2105+
// bytes were always the storage bytes, so `StringContent("\xc3\xa9", "utf-16")` announced
2106+
// charset=utf-16 and emitted the two UTF-8 bytes c3 a9. A conforming server decoded that pair as
2107+
// one UTF-16 code unit and got U+A9C3 -- a wrong character, silently.
2108+
//
2109+
// .NET makes the contradiction UNREPRESENTABLE rather than validating against it: the
2110+
// constructor takes an Encoding, serialises through it (StringContent.cs:90-98) and labels the
2111+
// header with that same object's WebName (:73). One source of truth.
2112+
TEST(NetHttpGatedBehaviourPins, Fix2070_TheBytesAreEncodedThroughTheDeclaredCharset) {
2113+
StringContent utf16("\xc3\xa9", System::Text::Encoding::Unicode(), "text/plain");
2114+
EXPECT_EQ(utf16.getCharSetProperty(), "utf-16");
2115+
const auto bytes = utf16.ReadAsByteArray();
2116+
ASSERT_EQ(bytes.size(), 2u) << "one UTF-16 code unit, not two UTF-8 storage bytes";
2117+
EXPECT_EQ(static_cast<unsigned>(bytes[0]), 0xe9u);
2118+
EXPECT_EQ(static_cast<unsigned>(bytes[1]), 0x00u);
2119+
2120+
// The default is UTF-8 and its bytes are the storage bytes, so nothing an ordinary caller
2121+
// does has moved.
2122+
StringContent def("\xc3\xa9");
2123+
EXPECT_EQ(def.getCharSetProperty(), "utf-8");
2124+
const auto defaultBytes = def.ReadAsByteArray();
2125+
ASSERT_EQ(defaultBytes.size(), 2u);
2126+
EXPECT_EQ(static_cast<unsigned>(defaultBytes[0]), 0xc3u);
2127+
EXPECT_EQ(static_cast<unsigned>(defaultBytes[1]), 0xa9u);
2128+
// ...and an explicit nullptr means the same thing, which is `encoding ??= DefaultStringEncoding`.
2129+
EXPECT_EQ(StringContent("\xc3\xa9", nullptr).ReadAsByteArray(), defaultBytes);
2130+
EXPECT_EQ(StringContent("\xc3\xa9", System::Text::Encoding::UTF8()).ReadAsByteArray(), defaultBytes);
2131+
}
2132+
2133+
TEST(NetHttpGatedBehaviourPins, Fix2070_TheLabelAndTheBytesCannotDisagreeForAnyEncoding) {
2134+
// The property, rather than one example of it: whatever encoding is handed in, the header
2135+
// names THAT encoding and the body is what THAT encoding produced. There is no third source.
2136+
struct Row { std::shared_ptr<System::Text::Encoding> encoding; const char* webName; };
2137+
const Row rows[] = {
2138+
{System::Text::Encoding::UTF8(), "utf-8"},
2139+
{System::Text::Encoding::Unicode(), "utf-16"},
2140+
{System::Text::Encoding::ASCII(), "us-ascii"},
2141+
{System::Text::Encoding::Latin1(), "iso-8859-1"},
2142+
{System::Text::Encoding::UTF32(), "utf-32"},
2143+
};
2144+
for (const Row& row : rows) {
2145+
SCOPED_TRACE(row.webName);
2146+
StringContent content("\xc3\xa9", row.encoding, "text/plain");
2147+
EXPECT_EQ(content.getCharSetProperty(), row.webName);
2148+
const auto expected = row.encoding->GetBytes(std::string("\xc3\xa9"));
2149+
EXPECT_EQ(content.ReadAsByteArray(), expected);
2150+
}
21092151
}
21102152

21112153
// #2071 (SR-AUD-318's limits half) LANDED. Response reads are bounded by

plan.sqlite3

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)