1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Diagnostics; 6 using System.Globalization; 7 using System.IO; 8 using System.Text; 9 10 namespace System.Net.Http.Headers 11 { 12 public class ContentRangeHeaderValue : ICloneable 13 { 14 private string _unit; 15 private long? _from; 16 private long? _to; 17 private long? _length; 18 19 public string Unit 20 { 21 get { return _unit; } 22 set 23 { 24 HeaderUtilities.CheckValidToken(value, nameof(value)); 25 _unit = value; 26 } 27 } 28 29 public long? From 30 { 31 get { return _from; } 32 } 33 34 public long? To 35 { 36 get { return _to; } 37 } 38 39 public long? Length 40 { 41 get { return _length; } 42 } 43 44 public bool HasLength // e.g. "Content-Range: bytes 12-34/*" 45 { 46 get { return _length != null; } 47 } 48 49 public bool HasRange // e.g. "Content-Range: bytes */1234" 50 { 51 get { return _from != null; } 52 } 53 ContentRangeHeaderValue(long from, long to, long length)54 public ContentRangeHeaderValue(long from, long to, long length) 55 { 56 // Scenario: "Content-Range: bytes 12-34/5678" 57 58 if (length < 0) 59 { 60 throw new ArgumentOutOfRangeException(nameof(length)); 61 } 62 if ((to < 0) || (to > length)) 63 { 64 throw new ArgumentOutOfRangeException(nameof(to)); 65 } 66 if ((from < 0) || (from > to)) 67 { 68 throw new ArgumentOutOfRangeException(nameof(from)); 69 } 70 71 _from = from; 72 _to = to; 73 _length = length; 74 _unit = HeaderUtilities.BytesUnit; 75 } 76 ContentRangeHeaderValue(long length)77 public ContentRangeHeaderValue(long length) 78 { 79 // Scenario: "Content-Range: bytes */1234" 80 81 if (length < 0) 82 { 83 throw new ArgumentOutOfRangeException(nameof(length)); 84 } 85 86 _length = length; 87 _unit = HeaderUtilities.BytesUnit; 88 } 89 ContentRangeHeaderValue(long from, long to)90 public ContentRangeHeaderValue(long from, long to) 91 { 92 // Scenario: "Content-Range: bytes 12-34/*" 93 94 if (to < 0) 95 { 96 throw new ArgumentOutOfRangeException(nameof(to)); 97 } 98 if ((from < 0) || (from > to)) 99 { 100 throw new ArgumentOutOfRangeException(nameof(from)); 101 } 102 103 _from = from; 104 _to = to; 105 _unit = HeaderUtilities.BytesUnit; 106 } 107 ContentRangeHeaderValue()108 private ContentRangeHeaderValue() 109 { 110 } 111 ContentRangeHeaderValue(ContentRangeHeaderValue source)112 private ContentRangeHeaderValue(ContentRangeHeaderValue source) 113 { 114 Debug.Assert(source != null); 115 116 _from = source._from; 117 _to = source._to; 118 _length = source._length; 119 _unit = source._unit; 120 } 121 Equals(object obj)122 public override bool Equals(object obj) 123 { 124 ContentRangeHeaderValue other = obj as ContentRangeHeaderValue; 125 126 if (other == null) 127 { 128 return false; 129 } 130 131 return ((_from == other._from) && (_to == other._to) && (_length == other._length) && 132 string.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase)); 133 } 134 GetHashCode()135 public override int GetHashCode() 136 { 137 int result = StringComparer.OrdinalIgnoreCase.GetHashCode(_unit); 138 139 if (HasRange) 140 { 141 result = result ^ _from.GetHashCode() ^ _to.GetHashCode(); 142 } 143 144 if (HasLength) 145 { 146 result = result ^ _length.GetHashCode(); 147 } 148 149 return result; 150 } 151 ToString()152 public override string ToString() 153 { 154 StringBuilder sb = StringBuilderCache.Acquire(); 155 sb.Append(_unit); 156 sb.Append(' '); 157 158 if (HasRange) 159 { 160 sb.Append(_from.Value.ToString(NumberFormatInfo.InvariantInfo)); 161 sb.Append('-'); 162 sb.Append(_to.Value.ToString(NumberFormatInfo.InvariantInfo)); 163 } 164 else 165 { 166 sb.Append('*'); 167 } 168 169 sb.Append('/'); 170 if (HasLength) 171 { 172 sb.Append(_length.Value.ToString(NumberFormatInfo.InvariantInfo)); 173 } 174 else 175 { 176 sb.Append('*'); 177 } 178 179 return StringBuilderCache.GetStringAndRelease(sb); 180 } 181 Parse(string input)182 public static ContentRangeHeaderValue Parse(string input) 183 { 184 int index = 0; 185 return (ContentRangeHeaderValue)GenericHeaderParser.ContentRangeParser.ParseValue(input, null, ref index); 186 } 187 TryParse(string input, out ContentRangeHeaderValue parsedValue)188 public static bool TryParse(string input, out ContentRangeHeaderValue parsedValue) 189 { 190 int index = 0; 191 object output; 192 parsedValue = null; 193 194 if (GenericHeaderParser.ContentRangeParser.TryParseValue(input, null, ref index, out output)) 195 { 196 parsedValue = (ContentRangeHeaderValue)output; 197 return true; 198 } 199 return false; 200 } 201 GetContentRangeLength(string input, int startIndex, out object parsedValue)202 internal static int GetContentRangeLength(string input, int startIndex, out object parsedValue) 203 { 204 Debug.Assert(startIndex >= 0); 205 206 parsedValue = null; 207 208 if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) 209 { 210 return 0; 211 } 212 213 // Parse the unit string: <unit> in '<unit> <from>-<to>/<length>' 214 int unitLength = HttpRuleParser.GetTokenLength(input, startIndex); 215 216 if (unitLength == 0) 217 { 218 return 0; 219 } 220 221 string unit = input.Substring(startIndex, unitLength); 222 int current = startIndex + unitLength; 223 int separatorLength = HttpRuleParser.GetWhitespaceLength(input, current); 224 225 if (separatorLength == 0) 226 { 227 return 0; 228 } 229 230 current = current + separatorLength; 231 232 if (current == input.Length) 233 { 234 return 0; 235 } 236 237 // Read range values <from> and <to> in '<unit> <from>-<to>/<length>' 238 int fromStartIndex = current; 239 int fromLength = 0; 240 int toStartIndex = 0; 241 int toLength = 0; 242 if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength)) 243 { 244 return 0; 245 } 246 247 // After the range is read we expect the length separator '/' 248 if ((current == input.Length) || (input[current] != '/')) 249 { 250 return 0; 251 } 252 253 current++; // Skip '/' separator 254 current = current + HttpRuleParser.GetWhitespaceLength(input, current); 255 256 if (current == input.Length) 257 { 258 return 0; 259 } 260 261 // We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now. 262 int lengthStartIndex = current; 263 int lengthLength = 0; 264 if (!TryGetLengthLength(input, ref current, out lengthLength)) 265 { 266 return 0; 267 } 268 269 if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength, 270 lengthStartIndex, lengthLength, out parsedValue)) 271 { 272 return 0; 273 } 274 275 return current - startIndex; 276 } 277 TryGetLengthLength(string input, ref int current, out int lengthLength)278 private static bool TryGetLengthLength(string input, ref int current, out int lengthLength) 279 { 280 lengthLength = 0; 281 282 if (input[current] == '*') 283 { 284 current++; 285 } 286 else 287 { 288 // Parse length value: <length> in '<unit> <from>-<to>/<length>' 289 lengthLength = HttpRuleParser.GetNumberLength(input, current, false); 290 291 if ((lengthLength == 0) || (lengthLength > HttpRuleParser.MaxInt64Digits)) 292 { 293 return false; 294 } 295 296 current = current + lengthLength; 297 } 298 299 current = current + HttpRuleParser.GetWhitespaceLength(input, current); 300 return true; 301 } 302 TryGetRangeLength(string input, ref int current, out int fromLength, out int toStartIndex, out int toLength)303 private static bool TryGetRangeLength(string input, ref int current, out int fromLength, out int toStartIndex, 304 out int toLength) 305 { 306 fromLength = 0; 307 toStartIndex = 0; 308 toLength = 0; 309 310 // Check if we have a value like 'bytes */133'. If yes, skip the range part and continue parsing the 311 // length separator '/'. 312 if (input[current] == '*') 313 { 314 current++; 315 } 316 else 317 { 318 // Parse first range value: <from> in '<unit> <from>-<to>/<length>' 319 fromLength = HttpRuleParser.GetNumberLength(input, current, false); 320 321 if ((fromLength == 0) || (fromLength > HttpRuleParser.MaxInt64Digits)) 322 { 323 return false; 324 } 325 326 current = current + fromLength; 327 current = current + HttpRuleParser.GetWhitespaceLength(input, current); 328 329 // After the first value, the '-' character must follow. 330 if ((current == input.Length) || (input[current] != '-')) 331 { 332 // We need a '-' character otherwise this can't be a valid range. 333 return false; 334 } 335 336 current++; // skip the '-' character 337 current = current + HttpRuleParser.GetWhitespaceLength(input, current); 338 339 if (current == input.Length) 340 { 341 return false; 342 } 343 344 // Parse second range value: <to> in '<unit> <from>-<to>/<length>' 345 toStartIndex = current; 346 toLength = HttpRuleParser.GetNumberLength(input, current, false); 347 348 if ((toLength == 0) || (toLength > HttpRuleParser.MaxInt64Digits)) 349 { 350 return false; 351 } 352 353 current = current + toLength; 354 } 355 356 current = current + HttpRuleParser.GetWhitespaceLength(input, current); 357 return true; 358 } 359 TryCreateContentRange(string input, string unit, int fromStartIndex, int fromLength, int toStartIndex, int toLength, int lengthStartIndex, int lengthLength, out object parsedValue)360 private static bool TryCreateContentRange(string input, string unit, int fromStartIndex, int fromLength, 361 int toStartIndex, int toLength, int lengthStartIndex, int lengthLength, out object parsedValue) 362 { 363 parsedValue = null; 364 365 long from = 0; 366 if ((fromLength > 0) && !HeaderUtilities.TryParseInt64(input, fromStartIndex, fromLength, out from)) 367 { 368 return false; 369 } 370 371 long to = 0; 372 if ((toLength > 0) && !HeaderUtilities.TryParseInt64(input, toStartIndex, toLength, out to)) 373 { 374 return false; 375 } 376 377 // 'from' must not be greater than 'to' 378 if ((fromLength > 0) && (toLength > 0) && (from > to)) 379 { 380 return false; 381 } 382 383 long length = 0; 384 if ((lengthLength > 0) && !HeaderUtilities.TryParseInt64(input, lengthStartIndex, lengthLength, out length)) 385 { 386 return false; 387 } 388 389 // 'from' and 'to' must be less than 'length' 390 if ((toLength > 0) && (lengthLength > 0) && (to >= length)) 391 { 392 return false; 393 } 394 395 ContentRangeHeaderValue result = new ContentRangeHeaderValue(); 396 result._unit = unit; 397 398 if (fromLength > 0) 399 { 400 result._from = from; 401 result._to = to; 402 } 403 404 if (lengthLength > 0) 405 { 406 result._length = length; 407 } 408 409 parsedValue = result; 410 return true; 411 } 412 ICloneable.Clone()413 object ICloneable.Clone() 414 { 415 return new ContentRangeHeaderValue(this); 416 } 417 } 418 } 419