• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tomdesair / tus-java-server / 28704942481

04 Jul 2026 11:35AM UTC coverage: 92.355%. First build
28704942481

Pull #105

github

web-flow
Merge 94b11f46d into e8bbe6ae5
Pull Request #105: feat: Implement IETF Resumable Uploads for HTTP (RUFH) Protocol

866 of 1022 branches covered (84.74%)

Branch coverage included in aggregate %.

443 of 496 new or added lines in 24 files covered. (89.31%)

2263 of 2366 relevant lines covered (95.65%)

6.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.42
/src/main/java/me/desair/tus/server/util/StructuredHeaderUtil.java
1
package me.desair.tus.server.util;
2

3
import java.util.LinkedHashMap;
4
import java.util.Map;
5
import org.apache.commons.lang3.Strings;
6

7
/**
8
 * Utility class for parsing and serializing RFC 9651 Structured Field Values used in IETF Resumable
9
 * Uploads for HTTP (RUFH).
10
 *
11
 * <p>RFC 9651 defines standardized data types for HTTP headers, including:
12
 *
13
 * <ul>
14
 *   <li><b>Booleans (Section 3.3.6)</b>: Encoded as {@code ?1} (true) or {@code ?0} (false).
15
 *   <li><b>Integers (Section 3.3.1)</b>: Encoded as signed 64-bit decimal integers.
16
 *   <li><b>Dictionaries (Section 3.2)</b>: Comma-separated list of key-value pairs (e.g. {@code
17
 *       max-size=100, max-append-size=500}). Keys without explicit values default to boolean {@code
18
 *       true}.
19
 * </ul>
20
 */
21
public class StructuredHeaderUtil {
22

23
  private StructuredHeaderUtil() {
24
    // Utility class
25
  }
26

27
  /**
28
   * Parse a Structured Header Boolean item according to RFC 9651 Section 3.3.6.
29
   *
30
   * @param headerValue The header value to parse (e.g. "?1" or "?0")
31
   * @return {@link Boolean#TRUE} for "?1", {@link Boolean#FALSE} for "?0", or {@code null} if
32
   *     invalid or blank
33
   */
34
  public static Boolean parseBoolean(String headerValue) {
35
    if (headerValue == null || headerValue.isBlank()) {
5✔
36
      return null;
2✔
37
    }
38
    String trimmed = headerValue.trim();
3✔
39
    if (Strings.CS.equals("?1", trimmed)) {
5✔
40
      return Boolean.TRUE;
2✔
41
    } else if (Strings.CS.equals("?0", trimmed)) {
5✔
42
      return Boolean.FALSE;
2✔
43
    }
44
    return null;
2✔
45
  }
46

47
  /**
48
   * Format a boolean into an RFC 9651 Structured Header Boolean item string.
49
   *
50
   * @param value The boolean value to format
51
   * @return "?1" for true, "?0" for false
52
   */
53
  public static String formatBoolean(boolean value) {
54
    return value ? "?1" : "?0";
6✔
55
  }
56

57
  /**
58
   * Parse a Structured Header Integer item according to RFC 9651 Section 3.3.1.
59
   *
60
   * @param headerValue The header value string (e.g. "12500")
61
   * @return Parsed {@link Long} value, or {@code null} if blank or not a valid decimal integer
62
   */
63
  public static Long parseInteger(String headerValue) {
64
    if (headerValue == null || headerValue.isBlank()) {
5✔
65
      return null;
2✔
66
    }
67
    try {
68
      return Long.parseLong(headerValue.trim());
5✔
69
    } catch (NumberFormatException e) {
1✔
70
      return null;
2✔
71
    }
72
  }
73

74
  /**
75
   * Parse a Structured Header Dictionary according to RFC 9651 Section 3.2.
76
   *
77
   * <p>Parses comma-separated dictionary members (e.g., {@code max-size=1000000,
78
   * max-append-size=50000}). Members without explicit values (e.g., {@code key}) implicitly
79
   * evaluate to boolean {@code true}.
80
   *
81
   * @param headerValue The dictionary header value string
82
   * @return Ordered {@link Map} of member key names to typed values ({@link Long}, {@link Boolean},
83
   *     or {@link String})
84
   */
85
  public static Map<String, Object> parseDictionary(String headerValue) {
86
    Map<String, Object> dictionary = new LinkedHashMap<>();
4✔
87
    if (headerValue == null || headerValue.isBlank()) {
5!
NEW
88
      return dictionary;
×
89
    }
90

91
    // Split dictionary members by comma (RFC 9651 Section 3.2)
92
    String[] members = headerValue.split(",");
4✔
93
    for (String member : members) {
16✔
94
      String trimmedMember = member.trim();
3✔
95
      if (trimmedMember.isEmpty()) {
3!
NEW
96
        continue;
×
97
      }
98

99
      int eqIdx = trimmedMember.indexOf('=');
4✔
100
      if (eqIdx > 0) {
2✔
101
        // Key-value pair member
102
        String key = trimmedMember.substring(0, eqIdx).trim();
6✔
103
        String valStr = trimmedMember.substring(eqIdx + 1).trim();
7✔
104

105
        // Attempt parsing as structured boolean item first (?1 / ?0)
106
        Boolean boolVal = parseBoolean(valStr);
3✔
107
        if (boolVal != null) {
2!
NEW
108
          dictionary.put(key, boolVal);
×
109
        } else {
110
          // Attempt parsing as structured integer item
111
          Long intVal = parseInteger(valStr);
3✔
112
          if (intVal != null) {
2!
113
            dictionary.put(key, intVal);
6✔
114
          } else {
115
            // Fallback to sanitized string value
NEW
116
            dictionary.put(key, sanitizeHeaderValue(valStr));
×
117
          }
118
        }
119
      } else {
1✔
120
        // According to RFC 9651 Section 3.2, a member with no value defaults to boolean true
121
        dictionary.put(trimmedMember, Boolean.TRUE);
5✔
122
      }
123
    }
124
    return dictionary;
2✔
125
  }
126

127
  /**
128
   * Format a map of key-value limits/entries into an RFC 9651 Structured Header Dictionary string.
129
   *
130
   * @param dictionary Map of dictionary keys and values
131
   * @return Structured header dictionary string (e.g. "max-size=100, max-append-size=50")
132
   */
133
  public static String formatDictionary(Map<String, Object> dictionary) {
134
    if (dictionary == null || dictionary.isEmpty()) {
10!
NEW
135
      return "";
×
136
    }
137
    StringBuilder sb = new StringBuilder();
8✔
138
    for (Map.Entry<String, Object> entry : dictionary.entrySet()) {
22✔
139
      if (entry.getValue() == null) {
6!
NEW
140
        continue;
×
141
      }
142
      if (sb.length() > 0) {
6✔
143
        sb.append(", ");
8✔
144
      }
145
      sb.append(sanitizeHeaderValue(entry.getKey())).append("=");
18✔
146
      Object val = entry.getValue();
6✔
147
      if (val instanceof Boolean) {
6✔
148
        sb.append(formatBoolean((Boolean) val));
8✔
149
      } else {
150
        sb.append(sanitizeHeaderValue(val.toString()));
12✔
151
      }
152
    }
2✔
153
    return sb.toString();
6✔
154
  }
155

156
  /**
157
   * Sanitize header input to prevent CRLF injection and HTTP response splitting attacks. Strips CR
158
   * ('\r'), LF ('\n'), and null ('\0') characters.
159
   *
160
   * @param value Raw string input
161
   * @return Sanitized string with CR/LF/null characters removed
162
   */
163
  public static String sanitizeHeaderValue(String value) {
164
    if (value == null) {
4✔
165
      return null;
2✔
166
    }
167
    return value.replace("\r", "").replace("\n", "").replace("\0", "").trim();
24✔
168
  }
169
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc