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.math.BigInteger; 024import java.util.Objects; 025import java.util.Optional; 026 027import static java.lang.String.format; 028import static java.util.Objects.requireNonNull; 029 030/** 031 * The result of applying a {@code Range} header to a known-length representation. 032 * 033 * @author <a href="https://www.revetkn.com">Mark Allen</a> 034 */ 035@ThreadSafe 036public final class ByteRangeSelection { 037 @NonNull 038 private static final BigInteger BIG_INTEGER_ZERO; 039 @NonNull 040 private static final BigInteger BIG_INTEGER_ONE; 041 042 static { 043 BIG_INTEGER_ZERO = BigInteger.ZERO; 044 BIG_INTEGER_ONE = BigInteger.ONE; 045 } 046 047 @NonNull 048 private final ByteRangeSelectionType type; 049 @Nullable 050 private final ByteRange range; 051 052 @NonNull 053 public static ByteRangeSelection fromHeaderValue(@Nullable String rangeHeaderValue, 054 @NonNull Long representationLength) { 055 requireNonNull(representationLength); 056 057 if (representationLength < 0) 058 throw new IllegalArgumentException("Representation length must be >= 0."); 059 060 String headerValue = Utilities.trimAggressivelyToNull(rangeHeaderValue); 061 062 if (headerValue == null) 063 return absent(); 064 065 int equalsIndex = headerValue.indexOf('='); 066 067 if (equalsIndex <= 0) 068 return malformed(); 069 070 String unit = headerValue.substring(0, equalsIndex).trim(); 071 String rangeSet = headerValue.substring(equalsIndex + 1).trim(); 072 073 if (!"bytes".equalsIgnoreCase(unit)) 074 return unsupported(); 075 076 if (rangeSet.isEmpty()) 077 return malformed(); 078 079 if (rangeSet.indexOf(',') >= 0) 080 return unsupported(); 081 082 int dashIndex = rangeSet.indexOf('-'); 083 084 if (dashIndex < 0 || dashIndex != rangeSet.lastIndexOf('-')) 085 return malformed(); 086 087 String first = rangeSet.substring(0, dashIndex).trim(); 088 String last = rangeSet.substring(dashIndex + 1).trim(); 089 090 if (first.isEmpty() && last.isEmpty()) 091 return malformed(); 092 093 BigInteger representationLengthAsBigInteger = BigInteger.valueOf(representationLength); 094 095 if (first.isEmpty()) 096 return suffixSelection(last, representationLength, representationLengthAsBigInteger); 097 098 return explicitSelection(first, last, representationLength, representationLengthAsBigInteger); 099 } 100 101 private ByteRangeSelection(@NonNull ByteRangeSelectionType type, 102 @Nullable ByteRange range) { 103 requireNonNull(type); 104 105 if (type == ByteRangeSelectionType.SATISFIABLE && range == null) 106 throw new IllegalArgumentException("A satisfiable byte range selection must include a range."); 107 108 if (type != ByteRangeSelectionType.SATISFIABLE && range != null) 109 throw new IllegalArgumentException("Only satisfiable byte range selections may include a range."); 110 111 this.type = type; 112 this.range = range; 113 } 114 115 @NonNull 116 public ByteRangeSelectionType getType() { 117 return this.type; 118 } 119 120 @NonNull 121 public Optional<ByteRange> getRange() { 122 return Optional.ofNullable(this.range); 123 } 124 125 @Override 126 public String toString() { 127 return format("%s{type=%s, range=%s}", getClass().getSimpleName(), getType(), getRange().orElse(null)); 128 } 129 130 @Override 131 public boolean equals(@Nullable Object object) { 132 if (this == object) 133 return true; 134 135 if (!(object instanceof ByteRangeSelection byteRangeSelection)) 136 return false; 137 138 return getType() == byteRangeSelection.getType() 139 && Objects.equals(getRange(), byteRangeSelection.getRange()); 140 } 141 142 @Override 143 public int hashCode() { 144 return Objects.hash(getType(), getRange()); 145 } 146 147 @NonNull 148 private static ByteRangeSelection absent() { 149 return new ByteRangeSelection(ByteRangeSelectionType.ABSENT, null); 150 } 151 152 @NonNull 153 private static ByteRangeSelection malformed() { 154 return new ByteRangeSelection(ByteRangeSelectionType.MALFORMED, null); 155 } 156 157 @NonNull 158 private static ByteRangeSelection unsupported() { 159 return new ByteRangeSelection(ByteRangeSelectionType.UNSUPPORTED, null); 160 } 161 162 @NonNull 163 private static ByteRangeSelection unsatisfiable() { 164 return new ByteRangeSelection(ByteRangeSelectionType.UNSATISFIABLE, null); 165 } 166 167 @NonNull 168 private static ByteRangeSelection satisfiable(@NonNull ByteRange range) { 169 requireNonNull(range); 170 return new ByteRangeSelection(ByteRangeSelectionType.SATISFIABLE, range); 171 } 172 173 @NonNull 174 private static ByteRangeSelection suffixSelection(@NonNull String suffixLengthText, 175 @NonNull Long representationLength, 176 @NonNull BigInteger representationLengthAsBigInteger) { 177 BigInteger suffixLength = parseNonnegativeInteger(suffixLengthText).orElse(null); 178 179 if (suffixLength == null) 180 return malformed(); 181 182 if (suffixLength.equals(BIG_INTEGER_ZERO)) 183 return unsatisfiable(); 184 185 if (representationLength == 0) 186 return unsatisfiable(); 187 188 BigInteger selectedLength = suffixLength.min(representationLengthAsBigInteger); 189 BigInteger start = representationLengthAsBigInteger.subtract(selectedLength); 190 BigInteger endInclusive = representationLengthAsBigInteger.subtract(BIG_INTEGER_ONE); 191 192 return satisfiable(ByteRange.fromStartAndEndInclusive(start.longValueExact(), endInclusive.longValueExact())); 193 } 194 195 @NonNull 196 private static ByteRangeSelection explicitSelection(@NonNull String firstText, 197 @NonNull String lastText, 198 @NonNull Long representationLength, 199 @NonNull BigInteger representationLengthAsBigInteger) { 200 BigInteger first = parseNonnegativeInteger(firstText).orElse(null); 201 202 if (first == null) 203 return malformed(); 204 205 BigInteger last = lastText.isEmpty() 206 ? null 207 : parseNonnegativeInteger(lastText).orElse(null); 208 209 if (!lastText.isEmpty() && last == null) 210 return malformed(); 211 212 if (last != null && first.compareTo(last) > 0) 213 return malformed(); 214 215 if (representationLength == 0 || first.compareTo(representationLengthAsBigInteger) >= 0) 216 return unsatisfiable(); 217 218 BigInteger endInclusive = last == null 219 ? representationLengthAsBigInteger.subtract(BIG_INTEGER_ONE) 220 : last.min(representationLengthAsBigInteger.subtract(BIG_INTEGER_ONE)); 221 222 return satisfiable(ByteRange.fromStartAndEndInclusive(first.longValueExact(), endInclusive.longValueExact())); 223 } 224 225 @NonNull 226 private static Optional<BigInteger> parseNonnegativeInteger(@NonNull String text) { 227 requireNonNull(text); 228 229 if (text.isEmpty()) 230 return Optional.empty(); 231 232 for (int i = 0; i < text.length(); i++) { 233 char c = text.charAt(i); 234 235 if (c < '0' || c > '9') 236 return Optional.empty(); 237 } 238 239 return Optional.of(new BigInteger(text)); 240 } 241 242 public enum ByteRangeSelectionType { 243 ABSENT, 244 MALFORMED, 245 UNSUPPORTED, 246 UNSATISFIABLE, 247 SATISFIABLE 248 } 249}