Merge branch 'BigIntegerParse'
[mono.git] / mcs / class / System.Net.Http / System.Net.Http.Headers / ContentDispositionHeaderValue.cs
1 //
2 // ContentDispositionHeaderValue.cs
3 //
4 // Authors:
5 //      Marek Safar  <marek.safar@gmail.com>
6 //
7 // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 //
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 //
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28
29 using System.Collections.Generic;
30 using System.Text;
31 using System.Globalization;
32
33 namespace System.Net.Http.Headers
34 {
35         public class ContentDispositionHeaderValue : ICloneable
36         {
37                 string dispositionType;
38                 List<NameValueHeaderValue> parameters;
39
40                 private ContentDispositionHeaderValue ()
41                 {
42                 }
43
44                 public ContentDispositionHeaderValue (string dispositionType)
45                 {
46                         DispositionType = dispositionType;
47                 }
48
49                 protected ContentDispositionHeaderValue (ContentDispositionHeaderValue source)
50                 {
51                         if (source == null)
52                                 throw new ArgumentNullException ("source");
53
54                         dispositionType = source.dispositionType;
55                         if (source.parameters != null) {
56                                 foreach (var item in source.parameters)
57                                         Parameters.Add (new NameValueHeaderValue (item));
58                         }
59                 }
60
61                 public DateTimeOffset? CreationDate {
62                         get {
63                                 return GetDateValue ("creation-date");
64                         }
65                         set {
66                                 SetDateValue ("creation-date", value);
67                         }
68                 }
69                 
70                 public string DispositionType {
71                         get {
72                                 return dispositionType;
73                         }
74                         set {
75                                 Parser.Token.Check (value);
76                                 dispositionType = value;
77                         }
78                 }
79
80                 public string FileName {
81                         get {
82                                 var value = FindParameter ("filename");
83                                 if (value == null)
84                                         return null;
85
86                                 return DecodeValue (value, false);
87                         }
88                         set {
89                                 if (value != null)
90                                         value = EncodeBase64Value (value);
91
92                                 SetValue ("filename", value);
93                         }
94                 }
95
96                 public string FileNameStar {
97                         get {
98                                 var value = FindParameter ("filename*");
99                                 if (value == null)
100                                         return null;
101
102                                 return DecodeValue (value, true);
103                         }
104                         set {
105                                 if (value != null)
106                                         value = EncodeRFC5987 (value);
107
108                                 SetValue ("filename*", value);
109                         }
110                 }
111
112                 public DateTimeOffset? ModificationDate {
113                         get {
114                                 return GetDateValue ("modification-date");
115                         }
116                         set {
117                                 SetDateValue ("modification-date", value);
118                         }
119                 }
120
121                 public string Name {
122                         get {
123                                 return FindParameter ("name");
124                         }
125                         set {
126                                 SetValue ("name", value);
127                         }
128                 }
129
130                 public ICollection<NameValueHeaderValue> Parameters {
131                         get {
132                                 return parameters ?? (parameters = new List<NameValueHeaderValue> ());
133                         }
134                 }
135
136                 public DateTimeOffset? ReadDate {
137                         get {
138                                 return GetDateValue ("read-date");
139                         }
140                         set {
141                                 SetDateValue ("read-date", value);
142                         }
143                 }
144
145                 public long? Size {
146                         get {
147                                 var found = FindParameter ("size");
148                                 long result;
149                                 if (Parser.Long.TryParse (found, out result))
150                                         return result;
151
152                                 return null;
153                         }
154                         set {
155                                 if (value == null) {
156                                         SetValue ("size", null);
157                                         return;
158                                 }
159
160                                 if (value < 0)
161                                         throw new ArgumentOutOfRangeException ("value");
162
163                                 SetValue ("size", value.Value.ToString (CultureInfo.InvariantCulture));
164                         }
165                 }
166
167                 object ICloneable.Clone ()
168                 {
169                         return new ContentDispositionHeaderValue (this);
170                 }
171
172                 public override bool Equals (object obj)
173                 {
174                         var source = obj as ContentDispositionHeaderValue;
175                         return source != null &&
176                                 string.Equals (source.dispositionType, dispositionType, StringComparison.OrdinalIgnoreCase) &&
177                                 source.parameters.SequenceEqual (parameters);
178                 }
179
180                 string FindParameter (string name)
181                 {
182                         if (parameters == null)
183                                 return null;
184
185                         foreach (var entry in parameters) {
186                                 if (string.Equals (entry.Name, name, StringComparison.OrdinalIgnoreCase))
187                                         return entry.Value;
188                         }
189
190                         return null;
191                 }
192
193                 DateTimeOffset? GetDateValue (string name)
194                 {
195                         var value = FindParameter (name);
196                         if (value == null || value == null)
197                                 return null;
198
199                         if (value.Length < 3)
200                                 return null;
201
202                         if (value[0] == '\"')
203                                 value = value.Substring (1, value.Length - 2);
204
205                         DateTimeOffset offset;
206                         if (Lexer.TryGetDateValue (value, out offset))
207                                 return offset;
208
209                         return null;
210                 }
211
212                 static string EncodeBase64Value (string value)
213                 {
214                         for (int i = 0; i < value.Length; ++i) {
215                                 var ch = value[i];
216                                 if (ch > 127) {
217                                         var encoding = Encoding.UTF8;
218                                         return string.Format ("\"=?{0}?B?{1}?=\"",
219                                                 encoding.WebName, Convert.ToBase64String (encoding.GetBytes (value)));
220                                 }
221                         }
222
223                         if (!Lexer.IsValidToken (value))
224                                 return "\"" + value + "\"";
225
226                         return value;
227                 }
228
229                 static string EncodeRFC5987 (string value)
230                 {
231                         var encoding = Encoding.UTF8;
232                         StringBuilder sb = new StringBuilder (value.Length + 11);
233                         sb.Append (encoding.WebName);
234                         sb.Append ('\'');
235                         sb.Append ('\'');
236
237                         for (int i = 0; i < value.Length; ++i) {
238                                 var ch = value[i];
239                                 if (ch > 127) {
240                                         foreach (var b in encoding.GetBytes (new[] { ch })) {
241                                                 sb.Append ('%');
242                                                 sb.Append (b.ToString ("X2"));
243                                         }
244
245                                         continue;
246                                 }
247
248                                 sb.Append (ch);
249                         }
250
251                         return sb.ToString ();
252                 }
253
254                 static string DecodeValue (string value, bool extendedNotation)
255                 {
256                         //
257                         // A short (length <= 78 characters)
258                         // parameter value containing only non-`tspecials' characters SHOULD be
259                         // represented as a single `token'.  A short parameter value containing
260                         // only ASCII characters, but including `tspecials' characters, SHOULD
261                         // be represented as `quoted-string'.  Parameter values longer than 78
262                         // characters, or which contain non-ASCII characters, MUST be encoded as
263                         // specified in [RFC 2184].
264                         //
265                         if (value.Length < 2)
266                                 return value;
267
268                         string[] sep;
269                         Encoding encoding;
270
271                         // Quoted string
272                         if (value[0] == '\"') {
273                                 //
274                                 // Is Base64 encoded ?
275                                 // encoded-word := "=?" charset "?" encoding "?" encoded-text "?="
276                                 //
277                                 sep = value.Split ('?');
278                                 if (sep.Length != 5 || sep[0] != "\"=" || sep[4] != "=\"" || (sep[2] != "B" && sep[2] != "b"))
279                                         return value;
280
281                                 try {
282                                         encoding = Encoding.GetEncoding (sep[1]);
283                                         return encoding.GetString (Convert.FromBase64String (sep[3]));
284                                 } catch {
285                                         return value;
286                                 }
287                         }
288
289                         if (!extendedNotation)
290                                 return value;
291
292                         //
293                         // RFC 5987: Charset/Language Encoding
294                         //
295                         sep = value.Split ('\'');
296                         if (sep.Length != 3)
297                                 return null;
298
299                         try {
300                                 encoding = Encoding.GetEncoding (sep[0]);
301                         } catch {
302                                 return null;
303                         }
304
305                         // TODO: What to do with sep[1] language
306
307                         value = sep[2];
308
309                         int pct_encoded = value.IndexOf ('%');
310                         if (pct_encoded < 0)
311                                 return value;
312
313                         StringBuilder sb = new StringBuilder ();
314                         byte[] buffer = null;
315                         int buffer_pos = 0;
316
317                         for (int i = 0; i < value.Length;) {
318                                 var ch = value[i];
319                                 if (ch == '%') {
320                                         var unescaped = ch;
321                                         ch = Uri.HexUnescape (value, ref i);
322                                         if (ch != unescaped) {
323                                                 if (buffer == null)
324                                                         buffer = new byte[value.Length - i + 1];
325
326                                                 buffer[buffer_pos++] = (byte) ch;
327                                                 continue;
328                                         }
329                                 } else {
330                                         ++i;
331                                 }
332
333                                 if (buffer_pos != 0) {
334                                         sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
335                                         buffer_pos = 0;
336                                 }
337
338                                 sb.Append (ch);
339                         }
340
341                         if (buffer_pos != 0) {
342                                 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
343                         }
344
345                         return sb.ToString ();
346                 }
347
348                 public override int GetHashCode ()
349                 {
350                         return dispositionType.ToLowerInvariant ().GetHashCode () ^
351                                 HashCodeCalculator.Calculate (parameters);
352                 }
353
354                 public static ContentDispositionHeaderValue Parse (string input)
355                 {
356                         ContentDispositionHeaderValue value;
357                         if (TryParse (input, out value))
358                                 return value;
359
360                         throw new FormatException (input);
361                 }
362
363                 void SetDateValue (string key, DateTimeOffset? value)
364                 {
365                         SetValue (key, value == null ? null : ("\"" + value.Value.ToString ("r", CultureInfo.InvariantCulture)) + "\"");
366                 }
367
368                 void SetValue (string key, string value)
369                 {
370                         if (parameters == null)
371                                 parameters = new List<NameValueHeaderValue> ();
372
373                         parameters.SetValue (key, value);
374                 }
375
376                 public override string ToString ()
377                 {
378                         return dispositionType + CollectionExtensions.ToString (parameters);
379                 }
380
381                 public static bool TryParse (string input, out ContentDispositionHeaderValue parsedValue)
382                 {
383                         parsedValue = null;
384
385                         var lexer = new Lexer (input);
386                         var t = lexer.Scan ();
387                         if (t.Kind != Token.Type.Token)
388                                 return false;
389
390                         List<NameValueHeaderValue> parameters = null;
391                         var type = lexer.GetStringValue (t);
392
393                         t = lexer.Scan ();
394
395                         switch (t.Kind) {
396                         case Token.Type.SeparatorSemicolon:
397                                 if (!NameValueHeaderValue.TryParseParameters (lexer, out parameters, out t) || t != Token.Type.End)
398                                         return false;
399                                 break;
400                         case Token.Type.End:
401                                 break;
402                         default:
403                                 return false;
404                         }
405
406                         parsedValue = new ContentDispositionHeaderValue () {
407                                 dispositionType = type,
408                                 parameters = parameters
409                         };
410
411                         return true;
412                 }
413         }
414 }