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}