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}