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;
024
025import static java.lang.String.format;
026import static java.lang.Math.addExact;
027import static java.lang.Math.subtractExact;
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Immutable representation of one satisfiable byte range.
032 *
033 * @author <a href="https://www.revetkn.com">Mark Allen</a>
034 */
035@ThreadSafe
036public final class ByteRange {
037        @NonNull
038        private final Long start;
039        @NonNull
040        private final Long endInclusive;
041        @NonNull
042        private final Long length;
043
044        @NonNull
045        public static ByteRange fromStartAndEndInclusive(@NonNull Long start,
046                                                                                                                                                                                                         @NonNull Long endInclusive) {
047                requireNonNull(start);
048                requireNonNull(endInclusive);
049                return new ByteRange(start, endInclusive);
050        }
051
052        private ByteRange(@NonNull Long start,
053                                                                                @NonNull Long endInclusive) {
054                requireNonNull(start);
055                requireNonNull(endInclusive);
056
057                if (start < 0)
058                        throw new IllegalArgumentException("Range start must be >= 0.");
059
060                if (endInclusive < 0)
061                        throw new IllegalArgumentException("Range end must be >= 0.");
062
063                if (start > endInclusive)
064                        throw new IllegalArgumentException("Range start must be <= range end.");
065
066                this.start = start;
067                this.endInclusive = endInclusive;
068
069                try {
070                        this.length = addExact(subtractExact(endInclusive, start), 1L);
071                } catch (ArithmeticException e) {
072                        throw new IllegalArgumentException("Range length exceeds maximum supported value.", e);
073                }
074        }
075
076        @NonNull
077        public Long getStart() {
078                return this.start;
079        }
080
081        @NonNull
082        public Long getEndInclusive() {
083                return this.endInclusive;
084        }
085
086        @NonNull
087        public Long getLength() {
088                return this.length;
089        }
090
091        @NonNull
092        public String toContentRangeHeaderValue(@NonNull Long representationLength) {
093                requireNonNull(representationLength);
094
095                if (representationLength < 0)
096                        throw new IllegalArgumentException("Representation length must be >= 0.");
097
098                if (getEndInclusive() >= representationLength)
099                        throw new IllegalArgumentException("Range end must be less than representation length.");
100
101                return format("bytes %d-%d/%d", getStart(), getEndInclusive(), representationLength);
102        }
103
104        @Override
105        public String toString() {
106                return format("%s{start=%s, endInclusive=%s}", getClass().getSimpleName(), getStart(), getEndInclusive());
107        }
108
109        @Override
110        public boolean equals(@Nullable Object object) {
111                if (this == object)
112                        return true;
113
114                if (!(object instanceof ByteRange byteRange))
115                        return false;
116
117                return Objects.equals(getStart(), byteRange.getStart())
118                                && Objects.equals(getEndInclusive(), byteRange.getEndInclusive());
119        }
120
121        @Override
122        public int hashCode() {
123                return Objects.hash(getStart(), getEndInclusive());
124        }
125}