Merge pull request #1322 from StephenMcConnel/bug23532
[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                                 var value = FindParameter ("name");
124
125                                 if (value == null)
126                                         return null;
127
128                                 return DecodeValue (value, false);
129                         }
130                         set {
131                                 if (value != null)
132                                         value = EncodeBase64Value (value);
133
134                                 SetValue ("name", value);
135                         }
136                 }
137
138                 public ICollection<NameValueHeaderValue> Parameters {
139                         get {
140                                 return parameters ?? (parameters = new List<NameValueHeaderValue> ());
141                         }
142                 }
143
144                 public DateTimeOffset? ReadDate {
145                         get {
146                                 return GetDateValue ("read-date");
147                         }
148                         set {
149                                 SetDateValue ("read-date", value);
150                         }
151                 }
152
153                 public long? Size {
154                         get {
155                                 var found = FindParameter ("size");
156                                 long result;
157                                 if (Parser.Long.TryParse (found, out result))
158                                         return result;
159
160                                 return null;
161                         }
162                         set {
163                                 if (value == null) {
164                                         SetValue ("size", null);
165                                         return;
166                                 }
167
168                                 if (value < 0)
169                                         throw new ArgumentOutOfRangeException ("value");
170
171                                 SetValue ("size", value.Value.ToString (CultureInfo.InvariantCulture));
172                         }
173                 }
174
175                 object ICloneable.Clone ()
176                 {
177                         return new ContentDispositionHeaderValue (this);
178                 }
179
180                 public override bool Equals (object obj)
181                 {
182                         var source = obj as ContentDispositionHeaderValue;
183                         return source != null &&
184                                 string.Equals (source.dispositionType, dispositionType, StringComparison.OrdinalIgnoreCase) &&
185                                 source.parameters.SequenceEqual (parameters);
186                 }
187
188                 string FindParameter (string name)
189                 {
190                         if (parameters == null)
191                                 return null;
192
193                         foreach (var entry in parameters) {
194                                 if (string.Equals (entry.Name, name, StringComparison.OrdinalIgnoreCase))
195                                         return entry.Value;
196                         }
197
198                         return null;
199                 }
200
201                 DateTimeOffset? GetDateValue (string name)
202                 {
203                         var value = FindParameter (name);
204                         if (value == null || value == null)
205                                 return null;
206
207                         if (value.Length < 3)
208                                 return null;
209
210                         if (value[0] == '\"')
211                                 value = value.Substring (1, value.Length - 2);
212
213                         DateTimeOffset offset;
214                         if (Lexer.TryGetDateValue (value, out offset))
215                                 return offset;
216
217                         return null;
218                 }
219
220                 static string EncodeBase64Value (string value)
221                 {
222                         bool quoted = value.Length > 1 && value [0] == '"' && value [value.Length - 1] == '"';
223                         if (quoted)
224                                 value = value.Substring (1, value.Length - 2);
225
226                         for (int i = 0; i < value.Length; ++i) {
227                                 var ch = value[i];
228                                 if (ch > 127) {
229                                         var encoding = Encoding.UTF8;
230                                         return string.Format ("\"=?{0}?B?{1}?=\"",
231                                                 encoding.WebName, Convert.ToBase64String (encoding.GetBytes (value)));
232                                 }
233                         }
234
235                         if (quoted || !Lexer.IsValidToken (value))
236                                 return "\"" + value + "\"";
237
238                         return value;
239                 }
240
241                 static string EncodeRFC5987 (string value)
242                 {
243                         var encoding = Encoding.UTF8;
244                         StringBuilder sb = new StringBuilder (value.Length + 11);
245                         sb.Append (encoding.WebName);
246                         sb.Append ('\'');
247                         sb.Append ('\'');
248
249                         for (int i = 0; i < value.Length; ++i) {
250                                 var ch = value[i];
251                                 if (ch > 127) {
252                                         foreach (var b in encoding.GetBytes (new[] { ch })) {
253                                                 sb.Append ('%');
254                                                 sb.Append (b.ToString ("X2"));
255                                         }
256
257                                         continue;
258                                 }
259
260                                 if (!Lexer.IsValidCharacter (ch) || ch == '*' || ch == '?' || ch == '%') {
261                                         sb.Append (Uri.HexEscape (ch));
262                                         continue;
263                                 }
264
265                                 sb.Append (ch);
266                         }
267
268                         return sb.ToString ();
269                 }
270
271                 static string DecodeValue (string value, bool extendedNotation)
272                 {
273                         //
274                         // A short (length <= 78 characters)
275                         // parameter value containing only non-`tspecials' characters SHOULD be
276                         // represented as a single `token'.  A short parameter value containing
277                         // only ASCII characters, but including `tspecials' characters, SHOULD
278                         // be represented as `quoted-string'.  Parameter values longer than 78
279                         // characters, or which contain non-ASCII characters, MUST be encoded as
280                         // specified in [RFC 2184].
281                         //
282                         if (value.Length < 2)
283                                 return value;
284
285                         string[] sep;
286                         Encoding encoding;
287
288                         // Quoted string
289                         if (value[0] == '\"') {
290                                 //
291                                 // Is Base64 encoded ?
292                                 // encoded-word := "=?" charset "?" encoding "?" encoded-text "?="
293                                 //
294                                 sep = value.Split ('?');
295                                 if (sep.Length != 5 || sep[0] != "\"=" || sep[4] != "=\"" || (sep[2] != "B" && sep[2] != "b"))
296                                         return value;
297
298                                 try {
299                                         encoding = Encoding.GetEncoding (sep[1]);
300                                         return encoding.GetString (Convert.FromBase64String (sep[3]));
301                                 } catch {
302                                         return value;
303                                 }
304                         }
305
306                         if (!extendedNotation)
307                                 return value;
308
309                         //
310                         // RFC 5987: Charset/Language Encoding
311                         //
312                         sep = value.Split ('\'');
313                         if (sep.Length != 3)
314                                 return null;
315
316                         try {
317                                 encoding = Encoding.GetEncoding (sep[0]);
318                         } catch {
319                                 return null;
320                         }
321
322                         // TODO: What to do with sep[1] language
323
324                         value = sep[2];
325
326                         int pct_encoded = value.IndexOf ('%');
327                         if (pct_encoded < 0)
328                                 return value;
329
330                         StringBuilder sb = new StringBuilder ();
331                         byte[] buffer = null;
332                         int buffer_pos = 0;
333
334                         for (int i = 0; i < value.Length;) {
335                                 var ch = value[i];
336                                 if (ch == '%') {
337                                         var unescaped = ch;
338                                         ch = Uri.HexUnescape (value, ref i);
339                                         if (ch != unescaped) {
340                                                 if (buffer == null)
341                                                         buffer = new byte[value.Length - i + 1];
342
343                                                 buffer[buffer_pos++] = (byte) ch;
344                                                 continue;
345                                         }
346                                 } else {
347                                         ++i;
348                                 }
349
350                                 if (buffer_pos != 0) {
351                                         sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
352                                         buffer_pos = 0;
353                                 }
354
355                                 sb.Append (ch);
356                         }
357
358                         if (buffer_pos != 0) {
359                                 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
360                         }
361
362                         return sb.ToString ();
363                 }
364
365                 public override int GetHashCode ()
366                 {
367                         return dispositionType.ToLowerInvariant ().GetHashCode () ^
368                                 HashCodeCalculator.Calculate (parameters);
369                 }
370
371                 public static ContentDispositionHeaderValue Parse (string input)
372                 {
373                         ContentDispositionHeaderValue value;
374                         if (TryParse (input, out value))
375                                 return value;
376
377                         throw new FormatException (input);
378                 }
379
380                 void SetDateValue (string key, DateTimeOffset? value)
381                 {
382                         SetValue (key, value == null ? null : ("\"" + value.Value.ToString ("r", CultureInfo.InvariantCulture)) + "\"");
383                 }
384
385                 void SetValue (string key, string value)
386                 {
387                         if (parameters == null)
388                                 parameters = new List<NameValueHeaderValue> ();
389
390                         parameters.SetValue (key, value);
391                 }
392
393                 public override string ToString ()
394                 {
395                         return dispositionType + CollectionExtensions.ToString (parameters);
396                 }
397
398                 public static bool TryParse (string input, out ContentDispositionHeaderValue parsedValue)
399                 {
400                         parsedValue = null;
401
402                         var lexer = new Lexer (input);
403                         var t = lexer.Scan ();
404                         if (t.Kind != Token.Type.Token)
405                                 return false;
406
407                         List<NameValueHeaderValue> parameters = null;
408                         var type = lexer.GetStringValue (t);
409
410                         t = lexer.Scan ();
411
412                         switch (t.Kind) {
413                         case Token.Type.SeparatorSemicolon:
414                                 if (!NameValueHeaderValue.TryParseParameters (lexer, out parameters, out t) || t != Token.Type.End)
415                                         return false;
416                                 break;
417                         case Token.Type.End:
418                                 break;
419                         default:
420                                 return false;
421                         }
422
423                         parsedValue = new ContentDispositionHeaderValue () {
424                                 dispositionType = type,
425                                 parameters = parameters
426                         };
427
428                         return true;
429                 }
430         }
431 }