001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
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 */
016package de.cuioss.http.security.data;
017
018import de.cuioss.http.security.core.ValidationType;
019import org.jspecify.annotations.Nullable;
020
021import java.util.Arrays;
022import java.util.List;
023import java.util.Optional;
024
025/**
026 * Immutable record representing an HTTP cookie with name, value, and attributes.
027 *
028 * <p>This record encapsulates the structure of HTTP cookies as defined in RFC 6265,
029 * providing a type-safe way to handle cookie data in HTTP security validation.</p>
030 *
031 * <h3>Design Principles</h3>
032 * <ul>
033 *   <li><strong>Immutability</strong> - All fields are final and the record cannot be modified</li>
034 *   <li><strong>RFC Compliance</strong> - Follows HTTP cookie specifications</li>
035 *   <li><strong>Security Focus</strong> - Designed with security validation in mind</li>
036 *   <li><strong>Flexibility</strong> - Supports various cookie attribute formats</li>
037 * </ul>
038 *
039 * <h3>Usage Examples</h3>
040 * <pre>
041 * // Simple cookie
042 * Cookie sessionCookie = new Cookie("JSESSIONID", "ABC123", "");
043 *
044 * // Cookie with attributes
045 * Cookie secureCookie = new Cookie(
046 *     "auth_token",
047 *     "xyz789",
048 *     "Domain=example.com; Path=/; Secure; HttpOnly"
049 * );
050 *
051 * // Access components
052 * String name = cookie.name();         // "JSESSIONID"
053 * String value = cookie.value();       // "ABC123"
054 * String attrs = cookie.attributes();  // "Domain=..."
055 *
056 * // Check for security attributes
057 * boolean isSecure = cookie.isSecure();       // Check for Secure attribute
058 * boolean isHttpOnly = cookie.isHttpOnly();   // Check for HttpOnly attribute
059 *
060 * // Use in validation
061 * validator.validate(cookie.name(), ValidationType.COOKIE_NAME);
062 * validator.validate(cookie.value(), ValidationType.COOKIE_VALUE);
063 * </pre>
064 *
065 * <h3>Cookie Attributes</h3>
066 * <p>The attributes field contains the semicolon-separated list of cookie attributes
067 * such as Domain, Path, Secure, HttpOnly, SameSite, and Max-Age. This field can be
068 * an empty string if no attributes are present.</p>
069 *
070 * <h3>Security Considerations</h3>
071 * <p>This record is a simple data container. Security validation should be applied
072 * to the name, value, and attributes components separately using appropriate validators.</p>
073 *
074 * Implements: Task B3 from HTTP verification specification
075 *
076 * @param name The cookie name (e.g., "JSESSIONID", "auth_token")
077 * @param value The cookie value (e.g., session ID, authentication token)
078 * @param attributes Cookie attributes string (e.g., "Domain=example.com; Secure; HttpOnly")
079 *
080 * @since 1.0
081 * @see ValidationType#COOKIE_NAME
082 * @see ValidationType#COOKIE_VALUE
083 */
084public record Cookie(@Nullable String name, @Nullable String value, @Nullable String attributes) {
085
086    /**
087     * Creates a simple cookie with no attributes.
088     *
089     * @param name The cookie name
090     * @param value The cookie value
091     * @return A Cookie with no attributes
092     */
093    public static Cookie simple(String name, String value) {
094        return new Cookie(name, value, "");
095    }
096
097    /**
098     * Checks if this cookie has a non-null, non-empty name.
099     *
100     * @return true if the name is not null and not empty
101     */
102    public boolean hasName() {
103        return name != null && !name.isEmpty();
104    }
105
106    /**
107     * Checks if this cookie has a non-null, non-empty value.
108     *
109     * @return true if the value is not null and not empty
110     */
111    public boolean hasValue() {
112        return value != null && !value.isEmpty();
113    }
114
115    /**
116     * Checks if this cookie has any attributes.
117     *
118     * @return true if the attributes string is not null and not empty
119     */
120    public boolean hasAttributes() {
121        return attributes != null && !attributes.isEmpty();
122    }
123
124    /**
125     * Checks if the cookie has the Secure attribute.
126     *
127     * @return true if the attributes contain "Secure"
128     */
129    @SuppressWarnings("ConstantConditions")
130    public boolean isSecure() {
131        return hasAttributes() && attributes.toLowerCase().contains("secure");
132    }
133
134    /**
135     * Checks if the cookie has the HttpOnly attribute.
136     *
137     * @return true if the attributes contain "HttpOnly"
138     */
139    @SuppressWarnings("ConstantConditions")
140    public boolean isHttpOnly() {
141        return hasAttributes() && attributes.toLowerCase().contains("httponly");
142    }
143
144    /**
145     * Extracts the Domain attribute value if present.
146     *
147     * @return The domain value wrapped in Optional, or empty if not specified
148     */
149    public Optional<String> getDomain() {
150        return extractAttributeValue("domain");
151    }
152
153    /**
154     * Extracts the Path attribute value if present.
155     *
156     * @return The path value wrapped in Optional, or empty if not specified
157     */
158    public Optional<String> getPath() {
159        return extractAttributeValue("path");
160    }
161
162    /**
163     * Extracts the SameSite attribute value if present.
164     *
165     * @return The SameSite value (e.g., "Strict", "Lax", "None") wrapped in Optional, or empty if not specified
166     */
167    public Optional<String> getSameSite() {
168        return extractAttributeValue("samesite");
169    }
170
171    /**
172     * Extracts the Max-Age attribute value if present.
173     *
174     * @return The Max-Age value as a string wrapped in Optional, or empty if not specified
175     */
176    public Optional<String> getMaxAge() {
177        return extractAttributeValue("max-age");
178    }
179
180    /**
181     * Extracts a specific attribute value from the attributes string.
182     *
183     * @param attributeName The name of the attribute (case-insensitive)
184     * @return The attribute value or null if not found
185     */
186    private Optional<String> extractAttributeValue(String attributeName) {
187        if (!hasAttributes()) {
188            return Optional.empty();
189        }
190        return AttributeParser.extractAttributeValue(attributes, attributeName);
191    }
192
193    /**
194     * Returns all attribute names present in this cookie.
195     *
196     * @return A list of attribute names (may be empty)
197     */
198    @SuppressWarnings("ConstantConditions")
199    public List<String> getAttributeNames() {
200        if (!hasAttributes()) {
201            return List.of();
202        }
203        return Arrays.stream(attributes.split(";"))
204                .map(String::trim)
205                .filter(attr -> !attr.isEmpty())
206                .map(attr -> {
207                    int equalIndex = attr.indexOf('=');
208                    return equalIndex > 0 ? attr.substring(0, equalIndex).trim() : attr;
209                })
210                .toList();
211    }
212
213    public String nameOrDefault(String defaultName) {
214        return name != null ? name : defaultName;
215    }
216
217    public String valueOrDefault(String defaultValue) {
218        return value != null ? value : defaultValue;
219    }
220
221    /**
222     * Returns a string representation suitable for HTTP Set-Cookie headers.
223     * Note: This does not perform proper HTTP encoding - use appropriate
224     * encoding utilities for actual HTTP header generation.
225     *
226     * @return A string in the format "name=value; attributes"
227     */
228    public String toCookieString() {
229        StringBuilder sb = new StringBuilder();
230
231        if (name != null) {
232            sb.append(name);
233        }
234
235        sb.append("=");
236
237        if (value != null) {
238            sb.append(value);
239        }
240
241        if (hasAttributes()) {
242            sb.append("; ").append(attributes);
243        }
244
245        return sb.toString();
246    }
247
248    /**
249     * Returns a copy of this cookie with a new name.
250     *
251     * @param newName The new cookie name
252     * @return A new Cookie with the specified name and same value/attributes
253     */
254    public Cookie withName(String newName) {
255        return new Cookie(newName, value, attributes);
256    }
257
258    /**
259     * Returns a copy of this cookie with a new value.
260     *
261     * @param newValue The new cookie value
262     * @return A new Cookie with the same name/attributes and specified value
263     */
264    public Cookie withValue(String newValue) {
265        return new Cookie(name, newValue, attributes);
266    }
267
268    /**
269     * Returns a copy of this cookie with new attributes.
270     *
271     * @param newAttributes The new attributes string
272     * @return A new Cookie with the same name/value and specified attributes
273     */
274    public Cookie withAttributes(String newAttributes) {
275        return new Cookie(name, value, newAttributes);
276    }
277}