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