33// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44#pragma once
55#include < string>
6+ #include < optional>
7+ #include < vector>
8+ #include < utility>
9+ #include " System/ArgumentException.hpp"
610#include " System/Version.hpp"
711
812namespace System {
@@ -37,48 +41,82 @@ namespace System {
3741 * comment.
3842 */
3943 class ApplicationId {
40- std::string name_;
41- Version version_;
42- std::string processorArchitecture_;
43- std::string culture_;
44- std::string publicKeyToken_;
44+ std::string name_;
45+ Version version_;
46+ std::optional<std:: string> processorArchitecture_;
47+ std::optional<std:: string> culture_;
48+ std::vector<SharpRuntime::bytecs> publicKeyToken_;
4549
4650 public:
4751 /* *
48- * @brief Initializes a new instance with the specified identity components .
52+ * @brief Constructs an ApplicationId .
4953 *
50- * C++ counterpart of .NET ApplicationId(byte[], string, Version, string, string).
51- * @param publicKeyToken The application's public key token (as a string).
52- * @param name The application name.
53- * @param version The application version.
54- * @param processorArchitecture The processor architecture ("x86", "amd64", etc.).
55- * @param culture The culture string ("neutral", "en-US", etc.).
54+ * C++ counterpart of .NET `ApplicationId(byte[], string, Version, string?, string?)`
55+ * (`ApplicationId.cs:13-24`).
56+ *
57+ * @par Ticket #2291 took all four of the review's decisions, toward .NET
58+ * 1. **The name is validated.** .NET raises for a null or empty name; this port accepted
59+ * `""` silently. `ArgumentException::ThrowIfNullOrEmpty` is the port's existing
60+ * helper, so no message is invented.
61+ * 2. **The token is bytes, not text.** It was a `std::string` stored verbatim, so binary
62+ * key material was unrepresentable and no clone was made. .NET takes `byte[]` and
63+ * **clones on the way in and on the way out**, so a caller cannot mutate a stored
64+ * token through the array it passed or the one it received.
65+ * 3. **Culture and ProcessorArchitecture are optional.** They are `string?` in .NET, and
66+ * were non-nullable `std::string` here, so absent and empty were one state.
67+ * 4. **`ToString()` adopts .NET's grammar** — see its own doc-comment.
68+ *
69+ * @throws System::ArgumentException if @p name is empty.
5670 */
57- ApplicationId (const std::string& publicKeyToken,
71+ ApplicationId (std::vector<SharpRuntime::bytecs> publicKeyToken,
5872 const std::string& name,
5973 const Version& version,
60- const std::string& processorArchitecture,
61- const std::string& culture)
74+ std::optional<std:: string> processorArchitecture,
75+ std::optional<std:: string> culture)
6276 : name_(name), version_(version),
63- processorArchitecture_ (processorArchitecture),
64- culture_(culture), publicKeyToken_(publicKeyToken) {}
77+ processorArchitecture_ (std::move(processorArchitecture)),
78+ culture_(std::move(culture)),
79+ publicKeyToken_(std::move(publicKeyToken)) {
80+ System::ArgumentException::ThrowIfNullOrEmpty (name, " name" );
81+ }
6582
6683 /* * @brief Gets the application name. C++ counterpart of .NET ApplicationId.Name. */
6784 [[nodiscard]] const std::string& getNameProperty () const { return name_; }
6885
6986 /* * @brief Gets the application version. C++ counterpart of .NET ApplicationId.Version. */
7087 [[nodiscard]] const Version& getVersionProperty () const { return version_; }
7188
72- /* * @brief Gets the processor architecture. C++ counterpart of .NET ApplicationId.ProcessorArchitecture. */
73- [[nodiscard]] const std::string& getProcessorArchitectureProperty () const {
89+ /* *
90+ * @brief Gets the processor architecture, or `std::nullopt` if absent.
91+ *
92+ * C++ counterpart of .NET `ApplicationId.ProcessorArchitecture`, which is `string?`.
93+ * Nullable since ticket #2291.
94+ */
95+ [[nodiscard]] const std::optional<std::string>& getProcessorArchitectureProperty () const {
7496 return processorArchitecture_;
7597 }
7698
77- /* * @brief Gets the application culture. C++ counterpart of .NET ApplicationId.Culture. */
78- [[nodiscard]] const std::string& getCultureProperty () const { return culture_; }
99+ /* *
100+ * @brief Gets the application culture, or `std::nullopt` if absent.
101+ *
102+ * C++ counterpart of .NET `ApplicationId.Culture`, which is `string?`.
103+ * Nullable since ticket #2291.
104+ */
105+ [[nodiscard]] const std::optional<std::string>& getCultureProperty () const {
106+ return culture_;
107+ }
79108
80- /* * @brief Gets the public key token. C++ counterpart of .NET ApplicationId.PublicKeyToken. */
81- [[nodiscard]] const std::string& getPublicKeyTokenProperty () const { return publicKeyToken_; }
109+ /* *
110+ * @brief Gets a COPY of the public key token.
111+ *
112+ * C++ counterpart of .NET `ApplicationId.PublicKeyToken`, which is
113+ * `=> (byte[])_publicKeyToken.Clone()` (`ApplicationId.cs:34`) — a defensive copy on
114+ * **every access**. Returning by value is that clone; a `const&` would have handed the
115+ * caller the stored array and defeated the constructor's own copy.
116+ */
117+ [[nodiscard]] std::vector<SharpRuntime::bytecs> getPublicKeyTokenProperty () const {
118+ return publicKeyToken_;
119+ }
82120
83121 /* *
84122 * @brief Creates a copy of this ApplicationId.
@@ -90,64 +128,91 @@ namespace System {
90128 /* *
91129 * @brief Determines whether this instance and the specified object have the same value.
92130 *
93- * C++ counterpart of .NET ApplicationId.Equals(object).
94- * Two ApplicationId instances are equal when all five fields match .
131+ * C++ counterpart of .NET ` ApplicationId.Equals` (`ApplicationId.cs:71-77`), which
132+ * compares all five components and the token **element by element** .
95133 */
96134 [[nodiscard]] bool Equals (const ApplicationId& other) const {
97- return name_ == other.name_
98- && version_ == other.version_
135+ return name_ == other.name_
136+ && version_ == other.version_
99137 && processorArchitecture_ == other.processorArchitecture_
100- && culture_ == other.culture_
101- && publicKeyToken_ == other.publicKeyToken_ ;
138+ && culture_ == other.culture_
139+ && publicKeyToken_ == other.publicKeyToken_ ;
102140 }
103141
104142 bool operator ==(const ApplicationId& o) const { return Equals (o); }
105143 bool operator !=(const ApplicationId& o) const { return !Equals (o); }
106144
107145 /* *
108- * @brief Returns a hash code for this ApplicationId based on the name and version.
146+ * @brief Returns a hash code derived from the name and version only .
109147 *
110- * C++ counterpart of .NET ApplicationId.GetHashCode(), which also derives
111- * the code from those two components only. Equal instances therefore hash
112- * equally even though `Equals` compares all five fields; unequal ones may
113- * collide, which is permitted.
148+ * C++ counterpart of .NET `ApplicationId.GetHashCode()`
149+ * (`ApplicationId.cs:79-82`), which carries its own comment: *"purposely skipping
150+ * publicKeyToken, processor architecture and culture as they are less likely to make
151+ * things not equal than name and version."* Equal instances therefore hash equally even
152+ * though `Equals` compares all five; unequal ones may collide, which is permitted.
114153 *
115- * @warning This is declared `noexcept` while `Version::ToString()` builds
116- * a `std::string`, so an allocation failure here calls `std::terminate`
117- * rather than propagating. Ticket #2292; no `SR-AUD-*` identifier.
154+ * @note It is **no longer `noexcept`**, and that closes ticket #2292: it used to hash
155+ * `version_.ToString()`, which allocates, so an allocation failure called
156+ * `std::terminate` rather than propagating. It now composes `Version::GetHashCode()`
157+ * instead, which is both what .NET does and allocation-free — so the `noexcept`
158+ * could arguably have stayed, and is dropped anyway because a hash that composes
159+ * another type's virtual-free but user-defined hash should not promise more than
160+ * that hash does.
118161 */
119- [[nodiscard]] int GetHashCode () const noexcept {
120- std:: size_t h = std::hash<std::string>{}(name_);
121- h ^= std::hash<std::string>{}(version_. ToString ( )) + 0x9e3779b9 + (h << 6 ) + (h >> 2 );
122- return static_cast < int >(h );
162+ [[nodiscard]] SharpRuntime::intcs GetHashCode () const {
163+ return static_cast <SharpRuntime::intcs>(
164+ static_cast <SharpRuntime::intcs>( std::hash<std::string>{}(name_ )) ^
165+ version_. GetHashCode () );
123166 }
124167
125168 /* *
126- * @brief Returns a string representation of the application identity,
127- * in this port's own grammar.
169+ * @brief Returns .NET's textual representation of the application identity.
170+ *
171+ * C++ counterpart of .NET `ApplicationId.ToString()` (`ApplicationId.cs:38-69`),
172+ * transcribed since ticket #2291. The grammar is:
173+ *
174+ * <name>[, culture="<c>"], version="<v>"[, publicKeyToken="<HEX>"][, processorArchitecture ="<a>"]
128175 *
129- * C++ counterpart of .NET ApplicationId.ToString() **in role only** — the
130- * text is not .NET's. This emits
131- * `<name>, Version=<v>, Culture=<c>, ProcessorArchitecture=<a>`:
132- * capitalized unquoted keys, both optional components always present,
133- * and **the public key token never included**. .NET writes lowercase
134- * quoted `culture`, `version`, `publicKeyToken` (uppercase hex bytes) and
135- * `processorArchitecture`, omitting components that are null.
176+ * lowercase quoted keys, absent components omitted, and the token as **uppercase** hex.
136177 *
137- * The consequence is worth stating plainly: **two ApplicationIds that
138- * differ only by public key token produce identical text here**, so this
139- * string does not identify an ApplicationId and must not be used as a
140- * manifest identity or as an equality proxy. `Equals` compares all five
141- * fields and does distinguish them.
178+ * @note Two details are transcribed rather than tidied, because they are the reference's
179+ * and a caller may match on them. The token is emitted even when EMPTY -- .NET's
180+ * guard is `_publicKeyToken != null`, and a zero-length array is not null, so an
181+ * empty token yields `publicKeyToken=""`. And `processorArchitecture` carries a
182+ * **space before its `=`**, which the other three keys do not; that asymmetry is in
183+ * `ApplicationId.cs:63` and is reproduced deliberately.
142184 *
143- * Changing this text would break any consumer that parses or logs it, and
144- * matching .NET needs the byte- token representation SR-AUD-124 is about,
145- * so both are held under one decision — SR-AUD-125, ticket #2291 .
185+ * Before #2291 this emitted a different grammar of its own -- capitalized unquoted keys,
186+ * both optional components always present, and ** the token never included**, so two
187+ * identities differing only by token produced identical text .
146188 */
147189 [[nodiscard]] std::string ToString () const {
148- return name_ + " , Version=" + version_.ToString ()
149- + " , Culture=" + culture_
150- + " , ProcessorArchitecture=" + processorArchitecture_;
190+ static constexpr char kHexDigits [] = " 0123456789ABCDEF" ;
191+ std::string out = name_;
192+ if (culture_.has_value ()) {
193+ out += " , culture=\" " ;
194+ out += *culture_;
195+ out += ' "' ;
196+ }
197+ out += " , version=\" " ;
198+ out += version_.ToString ();
199+ out += ' "' ;
200+ // Always emitted: .NET's guard is a null test on the array, and this port's
201+ // std::vector is never null -- so an empty token prints as an empty quoted value,
202+ // which is what .NET does for `new byte[0]` too.
203+ out += " , publicKeyToken=\" " ;
204+ for (SharpRuntime::bytecs b : publicKeyToken_) {
205+ const auto v = static_cast <unsigned char >(b);
206+ out += kHexDigits [(v >> 4 ) & 0x0F ];
207+ out += kHexDigits [v & 0x0F ];
208+ }
209+ out += ' "' ;
210+ if (processorArchitecture_.has_value ()) {
211+ out += " , processorArchitecture =\" " ; // the reference's own space before '='
212+ out += *processorArchitecture_;
213+ out += ' "' ;
214+ }
215+ return out;
151216 }
152217 };
153218
0 commit comments