001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.ThreadSafe;
023import java.util.Objects;
024import java.util.Optional;
025
026import static java.lang.String.format;
027import static java.util.Objects.requireNonNull;
028
029/**
030 * A validated W3C {@code tracestate} list member.
031 * <p>
032 * This type models only the W3C key/value envelope. Vendor-specific value contents are opaque.
033 *
034 * @author <a href="https://www.revetkn.com">Mark Allen</a>
035 */
036@ThreadSafe
037public final class TraceStateEntry {
038        @NonNull
039        private final String key;
040        @NonNull
041        private final String value;
042
043        /**
044         * Parses a W3C {@code tracestate} list member.
045         *
046         * @param member the list member text, for example {@code vendor=value}
047         * @return the parsed entry, or {@link Optional#empty()} if malformed
048         */
049        @NonNull
050        public static Optional<TraceStateEntry> fromMember(@Nullable String member) {
051                String trimmedMember = trimOws(member);
052
053                if (trimmedMember == null || trimmedMember.isEmpty())
054                        return Optional.empty();
055
056                int separator = trimmedMember.indexOf('=');
057
058                if (separator <= 0 || separator != trimmedMember.lastIndexOf('='))
059                        return Optional.empty();
060
061                String key = trimmedMember.substring(0, separator);
062                String value = trimmedMember.substring(separator + 1);
063
064                if (!isValidKey(key) || !isValidValue(value))
065                        return Optional.empty();
066
067                return Optional.of(new TraceStateEntry(key, value));
068        }
069
070        /**
071         * Creates a W3C {@code tracestate} list member from a key and value.
072         *
073         * @param key   the W3C {@code tracestate} key
074         * @param value the opaque W3C {@code tracestate} value
075         * @return the trace-state entry
076         */
077        @NonNull
078        public static TraceStateEntry fromKeyAndValue(@NonNull String key,
079                                                                                                                                                                                                @NonNull String value) {
080                requireNonNull(key);
081                requireNonNull(value);
082
083                if (!isValidKey(key))
084                        throw new IllegalArgumentException(format("Invalid tracestate key '%s'", key));
085
086                if (!isValidValue(value))
087                        throw new IllegalArgumentException(format("Invalid tracestate value for key '%s'", key));
088
089                return new TraceStateEntry(key, value);
090        }
091
092        private TraceStateEntry(@NonNull String key,
093                                                                                                        @NonNull String value) {
094                this.key = requireNonNull(key);
095                this.value = requireNonNull(value);
096        }
097
098        /**
099         * Returns the W3C {@code tracestate} key.
100         *
101         * @return the key
102         */
103        @NonNull
104        public String getKey() {
105                return this.key;
106        }
107
108        /**
109         * Returns the opaque W3C {@code tracestate} value.
110         *
111         * @return the value
112         */
113        @NonNull
114        public String getValue() {
115                return this.value;
116        }
117
118        /**
119         * Returns this entry in HTTP header member form.
120         *
121         * @return the {@code key=value} representation
122         */
123        @NonNull
124        public String toHeaderMemberValue() {
125                return format("%s=%s", getKey(), getValue());
126        }
127
128        @Override
129        @NonNull
130        public String toString() {
131                return format("%s{key=%s, valueLength=%s}", getClass().getSimpleName(), getKey(), getValue().length());
132        }
133
134        @Override
135        public boolean equals(@Nullable Object object) {
136                if (this == object)
137                        return true;
138
139                if (!(object instanceof TraceStateEntry traceStateEntry))
140                        return false;
141
142                return Objects.equals(getKey(), traceStateEntry.getKey())
143                                && Objects.equals(getValue(), traceStateEntry.getValue());
144        }
145
146        @Override
147        public int hashCode() {
148                return Objects.hash(getKey(), getValue());
149        }
150
151        @Nullable
152        private static String trimOws(@Nullable String value) {
153                if (value == null)
154                        return null;
155
156                int start = 0;
157                int end = value.length();
158
159                while (start < end && isOws(value.charAt(start)))
160                        start++;
161
162                while (end > start && isOws(value.charAt(end - 1)))
163                        end--;
164
165                return value.substring(start, end);
166        }
167
168        private static boolean isOws(char c) {
169                return c == ' ' || c == '\t';
170        }
171
172        static boolean isValidKey(@Nullable String key) {
173                if (key == null || key.isEmpty())
174                        return false;
175
176                int at = key.indexOf('@');
177
178                if (at < 0)
179                        return isValidSimpleKey(key);
180
181                if (at != key.lastIndexOf('@'))
182                        return false;
183
184                return isValidTenantId(key.substring(0, at))
185                                && isValidSystemId(key.substring(at + 1));
186        }
187
188        private static boolean isValidSimpleKey(@NonNull String key) {
189                requireNonNull(key);
190
191                if (key.length() > 256 || !isLowercaseAlpha(key.charAt(0)))
192                        return false;
193
194                for (int i = 1; i < key.length(); i++)
195                        if (!isKeyContinuation(key.charAt(i)))
196                                return false;
197
198                return true;
199        }
200
201        private static boolean isValidTenantId(@NonNull String tenantId) {
202                requireNonNull(tenantId);
203
204                if (tenantId.isEmpty() || tenantId.length() > 241 || !isLowercaseAlphaOrDigit(tenantId.charAt(0)))
205                        return false;
206
207                for (int i = 1; i < tenantId.length(); i++)
208                        if (!isKeyContinuation(tenantId.charAt(i)))
209                                return false;
210
211                return true;
212        }
213
214        private static boolean isValidSystemId(@NonNull String systemId) {
215                requireNonNull(systemId);
216
217                if (systemId.isEmpty() || systemId.length() > 14 || !isLowercaseAlpha(systemId.charAt(0)))
218                        return false;
219
220                for (int i = 1; i < systemId.length(); i++)
221                        if (!isKeyContinuation(systemId.charAt(i)))
222                                return false;
223
224                return true;
225        }
226
227        static boolean isValidValue(@Nullable String value) {
228                if (value == null || value.isEmpty() || value.length() > 256 || value.charAt(value.length() - 1) == ' ')
229                        return false;
230
231                for (int i = 0; i < value.length(); i++) {
232                        char c = value.charAt(i);
233
234                        if (c < 0x20 || c > 0x7E || c == ',' || c == '=')
235                                return false;
236                }
237
238                return true;
239        }
240
241        private static boolean isLowercaseAlpha(char c) {
242                return c >= 'a' && c <= 'z';
243        }
244
245        private static boolean isLowercaseAlphaOrDigit(char c) {
246                return isLowercaseAlpha(c) || isDigit(c);
247        }
248
249        private static boolean isDigit(char c) {
250                return c >= '0' && c <= '9';
251        }
252
253        private static boolean isKeyContinuation(char c) {
254                return isLowercaseAlphaOrDigit(c)
255                                || c == '_'
256                                || c == '-'
257                                || c == '*'
258                                || c == '/';
259        }
260}