Skip to content

Commit 81a666f

Browse files
committed
feat(uri): UriCreationOptions reaches a Uri operation (#1997, group A-3)
Rule-14 sweep. UriCreationOptions existed but no Uri operation accepted it, so a value could be constructed and read and then had nowhere to go -- SR-AUD-149's consumer half. Uri gains the constructor and TryCreate overloads that take it. Purely additive, under SA-5; nothing needed migrating. Premise correction found by the compiler: the plan's wording implied the option type had to be written too, and it already existed -- a first cut declaring it alongside the overloads failed with "redefinition of class System::UriCreationOptions". Only the overloads were missing. The existing type is left untouched, including its public data member, because .NET's is an unvalidated settable auto-property that a public field is observationally identical to (#1969's reasoning for ChannelOptions). Both overloads resolve against Absolute, and that is the reference's choice (Uri.cs:476-480, UriExt.cs:236-240), so a relative string throws through the constructor and fails through TryCreate even though the sibling one-argument constructor accepts one. All three are asserted together so the asymmetry is observable rather than theoretical. The option itself stays inert and that is disclosed rather than implied: .NET's flag disables path/query canonicalisation and this port performs none, so a Uri built with it set and one built with it clear are byte-for-byte identical -- asserted across three inputs chosen for dot segments, percent sequences and a default port. If that test ever fails, the port has grown canonicalisation and the disclosure must be revisited. The header's obsolete warning is replaced accordingly; leaving it would be the SR-AUD-168 defect. Three mutations, all caught, two run once per overload because they are independent bodies. Downstream measured: 0 sites in both. #1997 stays open for A-2 (a measured module-boundary cost) and A-4 (a vtable and access-level change). Gate: 17,522 run, 17,522 passed, 0 failed, 0 skipped across 38 executables (+5 on 17,517; SharpRuntimeTests_Uri 289 -> 294; no other executable moved). Module graph unchanged at 41/93.
1 parent efd1c14 commit 81a666f

7 files changed

Lines changed: 261 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
<!-- Copyright (c) Robert Vokac and contributors -->
3+
4+
# Migration — `UriCreationOptions` reaches a Uri operation (ticket #1997, group A-3)
5+
6+
*2026-08-19.* `System::Uri` gains `Uri(string, const UriCreationOptions&)` and
7+
`Uri::TryCreate(string, const UriCreationOptions&, shared_ptr<Uri>&)`.
8+
9+
**Purely additive** — no existing declaration, layout, vtable or mangled symbol changes. Landed
10+
under **SA-5**. Nothing needs migrating.
11+
12+
---
13+
14+
## 1. What was wrong
15+
16+
`UriCreationOptions` existed, but **no `Uri` operation accepted it**. A value could be constructed
17+
and read and then had nowhere to go. The type's own header said so, and named this group as the
18+
fix — SR-AUD-149's consumer half.
19+
20+
## 2. A premise correction found by the compiler
21+
22+
The plan describes A-3 as adding *"`Uri(string, UriCreationOptions)` + `TryCreate` overload"*,
23+
which reads as though the option type had to be written too. **It already existed**, as
24+
`modules/uri/include/System/UriCreationOptions.hpp`, and a first cut that declared it alongside
25+
the overloads failed with *"redefinition of class System::UriCreationOptions"*. Only the two
26+
overloads were missing.
27+
28+
The existing type is left exactly as it was, including its **public data member**: .NET's
29+
`DangerousDisablePathAndQueryCanonicalization` is a settable auto-property with no validation,
30+
which a public field is observationally identical to. SA-8 reaches a representation .NET keeps
31+
private, readonly or absent — the same reasoning #1969 recorded for `ChannelOptions`' three base
32+
flags.
33+
34+
## 3. Both overloads resolve against `Absolute`, and that is the reference's choice
35+
36+
```csharp
37+
public Uri(string uriString, in UriCreationOptions creationOptions)
38+
=> CreateThis(uriString, false, UriKind.Absolute, in creationOptions); // Uri.cs:476-480
39+
40+
public static bool TryCreate(string? uriString, in UriCreationOptions creationOptions, out Uri? result)
41+
=> (result = CreateHelper(uriString, false, UriKind.Absolute, in creationOptions)) is not null;
42+
// UriExt.cs:236-240
43+
```
44+
45+
So a **relative** string throws through the constructor and fails through `TryCreate`, even
46+
though the sibling one-argument `Uri(string)` accepts one. That asymmetry is easy to get wrong in
47+
either direction, and a test asserts all three behaviours together so the distinction is
48+
observable rather than theoretical.
49+
50+
## 4. The option is inert here, and that is disclosed rather than implied
51+
52+
.NET's flag disables validation and normalisation of the path and query. **This port performs
53+
none** — no path or query canonicalisation and no percent-encoding or decoding at all, a declared
54+
limitation of `Uri` and an explicit exclusion in the review plan (§15).
55+
56+
Turning off something that never happens changes nothing. A `Uri` built with the flag set and one
57+
built with it clear are byte-for-byte identical, and a test asserts that across three inputs
58+
chosen to exercise dot segments, percent sequences and a default port — the cases where
59+
canonicalisation would show if it existed. **If that test ever fails, the port has grown
60+
canonicalisation and the disclosure must be revisited.**
61+
62+
So this group closes SR-AUD-149's **consumer** half and explicitly does not close the behavioural
63+
half. The header now says exactly that, replacing the obsolete half of its old warning. Saying it
64+
is not optional: a header describing an effect it cannot produce, without disclosing it, is the
65+
defect SR-AUD-168 recorded one module over.
66+
67+
## 5. Evidence
68+
69+
Three mutations, **all caught**:
70+
71+
| Mutation | Caught by |
72+
|---|---|
73+
| M1 — the constructor's kind becomes `RelativeOrAbsolute` | `Decl1997A3_BothOverloadsResolveAgainstAbsoluteNotRelativeOrAbsolute` |
74+
| M2 — `TryCreate`'s kind becomes `RelativeOrAbsolute` | the same case |
75+
| M3 — `TryCreate` always fails | `Fix1997A3_TheTryCreateOverloadExists` |
76+
77+
M1 and M2 are run once per overload because they are **two independent bodies** — fixing one and
78+
leaving the other is the easy half-repair.
79+
80+
Gate: **17,522 run, 17,522 passed, 0 failed, 0 skipped** across 38 executables — `+5` on 17,517
81+
(`SharpRuntimeTests_Uri` 289 → 294). No other executable moved. Module graph unchanged at 41/93.
82+
83+
## 6. Downstream, measured
84+
85+
`UriCreationOptions` appears in **zero** places in `cna` and **zero** in `mobile-eggbert`, and
86+
nothing existing changed in any case. Neither repository was modified.
87+
88+
## 7. Scope
89+
90+
This closes **A-3** of #1997. A-1 landed earlier the same day. **A-2** (`Uri::CheckHostName`)
91+
remains blocked on a measured module-boundary cost — it classifies through
92+
`IPv6AddressHelper`/`IPv4AddressHelper`, which `modules/uri` does not have and cannot reach
93+
without a new public edge — and **A-4** changes `UriParser`'s vtable and access levels.

modules/uri/include/System/Uri.hpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
#include "SharpRuntime/SharpRuntimeHelper.hpp"
99
#include "System/UriHostNameType.hpp"
1010
#include "System/UriPartial.hpp"
11+
#include "System/UriCreationOptions.hpp"
1112

1213
namespace System {
1314

15+
1416
using SharpRuntime::intcs;
1517

1618
/**
@@ -89,6 +91,25 @@ namespace System {
8991
*/
9092
Uri(const std::string& uriString, UriKind uriKind);
9193

94+
/**
95+
* @brief Constructs a Uri from a string with the given creation options.
96+
*
97+
* C++ counterpart of .NET's `Uri(string uriString, in UriCreationOptions creationOptions)`
98+
* (`Uri.cs:476-480`). Ticket #1997 group A-3 / SR-AUD-149.
99+
*
100+
* @param uriString The URI string.
101+
* @param creationOptions Options controlling creation. **Inert in this port** — see
102+
* `UriCreationOptions`.
103+
* @throws System::UriFormatException if @p uriString is not a valid **absolute** URI.
104+
*
105+
* @note **The kind is `Absolute`, not `RelativeOrAbsolute`**, and that is the reference's
106+
* choice rather than this port's: .NET's body is
107+
* `CreateThis(uriString, false, UriKind.Absolute, in creationOptions)`. A relative string
108+
* therefore throws here, exactly as it does through .NET's overload — which is easy to
109+
* get wrong, because the sibling one-argument constructor accepts one.
110+
*/
111+
Uri(const std::string& uriString, const UriCreationOptions& creationOptions);
112+
92113
/**
93114
* @brief Constructs an absolute URI by combining a base URI with a relative reference.
94115
*
@@ -193,6 +214,26 @@ namespace System {
193214
static bool TryCreate(const std::string& uriString, UriKind uriKind,
194215
std::shared_ptr<Uri>& result);
195216

217+
/**
218+
* @brief Tries to construct a Uri from a string with the given creation options.
219+
*
220+
* C++ counterpart of .NET's
221+
* `TryCreate(string?, in UriCreationOptions, out Uri?)` (`UriExt.cs:236-240`). Ticket
222+
* #1997 group A-3 / SR-AUD-149.
223+
*
224+
* @param uriString The URI string.
225+
* @param creationOptions Options controlling creation. **Inert in this port** — see
226+
* `UriCreationOptions`.
227+
* @param result Receives the constructed Uri on success, or null on failure.
228+
* @return @c true on success.
229+
*
230+
* @note Like the constructor, this resolves against `UriKind::Absolute` --
231+
* .NET's body is `CreateHelper(uriString, false, UriKind.Absolute, in creationOptions)`.
232+
*/
233+
static bool TryCreate(const std::string& uriString,
234+
const UriCreationOptions& creationOptions,
235+
std::shared_ptr<Uri>& result);
236+
196237
/**
197238
* @brief Determines whether @p schemeName is a syntactically valid URI scheme.
198239
*

modules/uri/include/System/UriCreationOptions.hpp

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,30 @@ namespace System {
1010
*
1111
* C++ counterpart of .NET System.UriCreationOptions (introduced in .NET 6).
1212
*
13-
* @warning **No System::Uri operation accepts this type.** The .NET original is
14-
* consumed by `Uri(string, UriCreationOptions)` and by an options-bearing
15-
* `Uri.TryCreate`; this port has neither overload, so a value of this type can be
16-
* constructed and read but can never reach a URI operation at all. That is
17-
* SR-AUD-149, and adding the consumer overloads is public API addition, gated as
18-
* ticket #1997 group A-3. Ticket #1994 added this disclosure only — the type's
19-
* behaviour is unchanged.
13+
* @note **Both consumer overloads now exist** — `Uri(string, const UriCreationOptions&)`
14+
* and `Uri::TryCreate(string, const UriCreationOptions&, shared_ptr<Uri>&)` — added by
15+
* ticket #1997 group A-3, which closed SR-AUD-149's consumer half. Ticket #1994 had
16+
* previously added the disclosure that neither existed; that half of the warning is now
17+
* obsolete and is replaced by this note.
18+
*
19+
* @warning **The option itself remains INERT, and that is the half SR-AUD-149 does not
20+
* close.** .NET's flag disables validation and normalisation of the path and query, but
21+
* this port's `Uri` performs **no path or query canonicalisation and no
22+
* percent-encoding/decoding at all** — a declared limitation of that class
23+
* (`docs/SystemUriNamespaceReviewPlan.md` §15). Turning off something that never happens
24+
* changes nothing: a `Uri` built with the flag set and one built with it clear are
25+
* byte-for-byte identical, and `UriCreationOptionsTest` asserts exactly that.
26+
*
27+
* The overloads exist so ported C# declarations compile and so the caller's expressed
28+
* intent is preserved and readable — not because the flag has an effect. Saying so is not
29+
* optional: a header that describes an effect it cannot produce, without disclosing it, is
30+
* the defect SR-AUD-168 recorded one module over.
31+
*
32+
* @note The flag is a **public data member** rather than a rule-5 accessor pair, and that is
33+
* deliberate: .NET's is a settable auto-property with no validation, which a public field
34+
* is observationally identical to. SA-8 reaches a representation .NET keeps private,
35+
* readonly or absent — the same reasoning #1969 recorded for `ChannelOptions`' three base
36+
* flags.
2037
*/
2138
struct UriCreationOptions {
2239
/**

modules/uri/src/System/Uri.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,16 @@ Uri::Uri(const std::string& uriString, UriKind uriKind) {
431431
throw System::UriFormatException("URI must be relative");
432432
}
433433

434+
// Ticket #1997 group A-3 / SR-AUD-149. Delegates to the UriKind overload with Absolute, which is
435+
// what .NET's body does: CreateThis(uriString, false, UriKind.Absolute, in creationOptions)
436+
// (Uri.cs:476-480). The options are accepted and preserved by the caller's own object; they carry
437+
// no effect here because this Uri performs no path/query canonicalisation to disable -- disclosed
438+
// on UriCreationOptions itself.
439+
Uri::Uri(const std::string& uriString, const UriCreationOptions& creationOptions)
440+
: Uri(uriString, UriKind::Absolute) {
441+
(void)creationOptions;
442+
}
443+
434444
Uri::Uri(const Uri& baseUri, const std::string& relativeUri) {
435445
if (!baseUri.isAbsoluteUri_)
436446
throw System::ArgumentOutOfRangeException("baseUri");
@@ -658,4 +668,14 @@ bool Uri::TryCreate(const std::string& uriString, UriKind uriKind,
658668
}
659669
}
660670

671+
// Ticket #1997 group A-3 / SR-AUD-149. .NET's body is
672+
// CreateHelper(uriString, false, UriKind.Absolute, in creationOptions) (UriExt.cs:236-240), so
673+
// this resolves against Absolute like the constructor -- NOT RelativeOrAbsolute, which is the
674+
// easy mistake because the sibling one-argument constructor accepts a relative string.
675+
bool Uri::TryCreate(const std::string& uriString, const UriCreationOptions& creationOptions,
676+
std::shared_ptr<Uri>& result) {
677+
(void)creationOptions;
678+
return TryCreate(uriString, UriKind::Absolute, result);
679+
}
680+
661681
} // namespace System

modules/uri/tests/System/UriCreationOptionsTests.cpp

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,85 @@ TEST(UriCreationOptionsTest, TwoInstancesIndependent) {
2424
EXPECT_FALSE(a.DangerousDisablePathAndQueryCanonicalization);
2525
EXPECT_TRUE(b.DangerousDisablePathAndQueryCanonicalization);
2626
}
27+
28+
// =============================================================================================
29+
// Ticket #1997 group A-3 (SR-AUD-149) — the two consumer overloads.
30+
//
31+
// Before this group, a UriCreationOptions could be constructed and read but could NEVER REACH A
32+
// URI OPERATION: neither Uri(string, UriCreationOptions) nor an options-bearing TryCreate
33+
// existed. That was the finding, and the type's own header said so.
34+
//
35+
// The overloads are transcribed from the reference, INCLUDING their kind:
36+
// Uri(string, in UriCreationOptions) -> CreateThis(uriString, false, UriKind.Absolute, ...)
37+
// (Uri.cs:476-480)
38+
// TryCreate(string, in UriCreationOptions, out Uri?)
39+
// -> CreateHelper(uriString, false, UriKind.Absolute, ...)
40+
// (UriExt.cs:236-240)
41+
// =============================================================================================
42+
#include <memory>
43+
#include "System/Uri.hpp"
44+
#include "System/UriFormatException.hpp"
45+
46+
TEST(UriCreationOptionsTest, Fix1997A3_TheConstructorOverloadExistsAndParses) {
47+
UriCreationOptions opts;
48+
System::Uri uri("http://example.com/path", opts);
49+
EXPECT_EQ(uri.getHostProperty(), "example.com");
50+
EXPECT_TRUE(uri.getIsAbsoluteUriProperty());
51+
}
52+
53+
TEST(UriCreationOptionsTest, Fix1997A3_TheTryCreateOverloadExists) {
54+
UriCreationOptions opts;
55+
std::shared_ptr<System::Uri> result;
56+
EXPECT_TRUE(System::Uri::TryCreate("http://example.com/path", opts, result));
57+
ASSERT_NE(result, nullptr);
58+
EXPECT_EQ(result->getHostProperty(), "example.com");
59+
}
60+
61+
TEST(UriCreationOptionsTest, Decl1997A3_BothOverloadsResolveAgainstAbsoluteNotRelativeOrAbsolute) {
62+
// THE DETAIL THAT IS EASY TO GET WRONG, because the sibling one-argument Uri constructor
63+
// accepts a relative string. .NET passes UriKind.Absolute in BOTH bodies, so a relative
64+
// string throws through the constructor and fails through TryCreate.
65+
UriCreationOptions opts;
66+
EXPECT_THROW(System::Uri("/relative/path", opts), System::UriFormatException);
67+
68+
std::shared_ptr<System::Uri> result;
69+
EXPECT_FALSE(System::Uri::TryCreate("/relative/path", opts, result));
70+
EXPECT_EQ(result, nullptr);
71+
72+
// ...while the one-argument constructor still accepts it, which is what makes the
73+
// distinction observable rather than theoretical.
74+
EXPECT_NO_THROW(System::Uri("/relative/path"));
75+
}
76+
77+
TEST(UriCreationOptionsTest, Decl1997A3_TheFlagIsInertAndTheResultIsIdentical) {
78+
// THE HALF SR-AUD-149 DOES NOT CLOSE, asserted rather than merely documented. .NET's flag
79+
// disables path/query canonicalisation; this port performs none, so setting it changes
80+
// nothing. If this ever starts failing, the port has grown canonicalisation and the header's
81+
// disclosure must be revisited.
82+
UriCreationOptions off;
83+
UriCreationOptions on;
84+
on.DangerousDisablePathAndQueryCanonicalization = true;
85+
86+
for (const char* text : {"http://example.com/a/./b/../c?q=1&r=%2F",
87+
"http://example.com/%zz/trailing%",
88+
"https://example.com:443/path#frag"}) {
89+
System::Uri a(text, off);
90+
System::Uri b(text, on);
91+
EXPECT_EQ(a.getOriginalStringProperty(), b.getOriginalStringProperty()) << text;
92+
EXPECT_EQ(a.getAbsolutePathProperty(), b.getAbsolutePathProperty()) << text;
93+
EXPECT_EQ(a.getQueryProperty(), b.getQueryProperty()) << text;
94+
EXPECT_EQ(a.getHostProperty(), b.getHostProperty()) << text;
95+
}
96+
}
97+
98+
TEST(UriCreationOptionsTest, Decl1997A3_TheOptionsAreNotMutatedByEitherOverload) {
99+
// The caller's object is theirs; neither overload may write through it.
100+
UriCreationOptions opts;
101+
opts.DangerousDisablePathAndQueryCanonicalization = true;
102+
System::Uri uri("http://example.com/", opts);
103+
EXPECT_TRUE(opts.DangerousDisablePathAndQueryCanonicalization);
104+
105+
std::shared_ptr<System::Uri> result;
106+
(void)System::Uri::TryCreate("http://example.com/", opts, result);
107+
EXPECT_TRUE(opts.DangerousDisablePathAndQueryCanonicalization);
108+
}

plan.sqlite3

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)