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}