2 // ContentDispositionHeaderValue.cs
5 // Marek Safar <marek.safar@gmail.com>
7 // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
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:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
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.
29 using System.Collections.Generic;
31 using System.Globalization;
33 namespace System.Net.Http.Headers
35 public class ContentDispositionHeaderValue : ICloneable
37 string dispositionType;
38 List<NameValueHeaderValue> parameters;
40 private ContentDispositionHeaderValue ()
44 public ContentDispositionHeaderValue (string dispositionType)
46 DispositionType = dispositionType;
49 protected ContentDispositionHeaderValue (ContentDispositionHeaderValue source)
52 throw new ArgumentNullException ("source");
54 dispositionType = source.dispositionType;
55 if (source.parameters != null) {
56 foreach (var item in source.parameters)
57 Parameters.Add (new NameValueHeaderValue (item));
61 public DateTimeOffset? CreationDate {
63 return GetDateValue ("creation-date");
66 SetDateValue ("creation-date", value);
70 public string DispositionType {
72 return dispositionType;
75 Parser.Token.Check (value);
76 dispositionType = value;
80 public string FileName {
82 var value = FindParameter ("filename");
86 return DecodeValue (value, false);
90 value = EncodeBase64Value (value);
92 SetValue ("filename", value);
96 public string FileNameStar {
98 var value = FindParameter ("filename*");
102 return DecodeValue (value, true);
106 value = EncodeRFC5987 (value);
108 SetValue ("filename*", value);
112 public DateTimeOffset? ModificationDate {
114 return GetDateValue ("modification-date");
117 SetDateValue ("modification-date", value);
123 var value = FindParameter ("name");
128 return DecodeValue (value, false);
132 value = EncodeBase64Value (value);
134 SetValue ("name", value);
138 public ICollection<NameValueHeaderValue> Parameters {
140 return parameters ?? (parameters = new List<NameValueHeaderValue> ());
144 public DateTimeOffset? ReadDate {
146 return GetDateValue ("read-date");
149 SetDateValue ("read-date", value);
155 var found = FindParameter ("size");
157 if (Parser.Long.TryParse (found, out result))
164 SetValue ("size", null);
169 throw new ArgumentOutOfRangeException ("value");
171 SetValue ("size", value.Value.ToString (CultureInfo.InvariantCulture));
175 object ICloneable.Clone ()
177 return new ContentDispositionHeaderValue (this);
180 public override bool Equals (object obj)
182 var source = obj as ContentDispositionHeaderValue;
183 return source != null &&
184 string.Equals (source.dispositionType, dispositionType, StringComparison.OrdinalIgnoreCase) &&
185 source.parameters.SequenceEqual (parameters);
188 string FindParameter (string name)
190 if (parameters == null)
193 foreach (var entry in parameters) {
194 if (string.Equals (entry.Name, name, StringComparison.OrdinalIgnoreCase))
201 DateTimeOffset? GetDateValue (string name)
203 var value = FindParameter (name);
204 if (value == null || value == null)
207 if (value.Length < 3)
210 if (value[0] == '\"')
211 value = value.Substring (1, value.Length - 2);
213 DateTimeOffset offset;
214 if (Lexer.TryGetDateValue (value, out offset))
220 static string EncodeBase64Value (string value)
222 bool quoted = value.Length > 1 && value [0] == '"' && value [value.Length - 1] == '"';
224 value = value.Substring (1, value.Length - 2);
226 for (int i = 0; i < value.Length; ++i) {
229 var encoding = Encoding.UTF8;
230 return string.Format ("\"=?{0}?B?{1}?=\"",
231 encoding.WebName, Convert.ToBase64String (encoding.GetBytes (value)));
235 if (quoted || !Lexer.IsValidToken (value))
236 return "\"" + value + "\"";
241 static string EncodeRFC5987 (string value)
243 var encoding = Encoding.UTF8;
244 StringBuilder sb = new StringBuilder (value.Length + 11);
245 sb.Append (encoding.WebName);
249 for (int i = 0; i < value.Length; ++i) {
252 foreach (var b in encoding.GetBytes (new[] { ch })) {
254 sb.Append (b.ToString ("X2"));
263 return sb.ToString ();
266 static string DecodeValue (string value, bool extendedNotation)
269 // A short (length <= 78 characters)
270 // parameter value containing only non-`tspecials' characters SHOULD be
271 // represented as a single `token'. A short parameter value containing
272 // only ASCII characters, but including `tspecials' characters, SHOULD
273 // be represented as `quoted-string'. Parameter values longer than 78
274 // characters, or which contain non-ASCII characters, MUST be encoded as
275 // specified in [RFC 2184].
277 if (value.Length < 2)
284 if (value[0] == '\"') {
286 // Is Base64 encoded ?
287 // encoded-word := "=?" charset "?" encoding "?" encoded-text "?="
289 sep = value.Split ('?');
290 if (sep.Length != 5 || sep[0] != "\"=" || sep[4] != "=\"" || (sep[2] != "B" && sep[2] != "b"))
294 encoding = Encoding.GetEncoding (sep[1]);
295 return encoding.GetString (Convert.FromBase64String (sep[3]));
301 if (!extendedNotation)
305 // RFC 5987: Charset/Language Encoding
307 sep = value.Split ('\'');
312 encoding = Encoding.GetEncoding (sep[0]);
317 // TODO: What to do with sep[1] language
321 int pct_encoded = value.IndexOf ('%');
325 StringBuilder sb = new StringBuilder ();
326 byte[] buffer = null;
329 for (int i = 0; i < value.Length;) {
333 ch = Uri.HexUnescape (value, ref i);
334 if (ch != unescaped) {
336 buffer = new byte[value.Length - i + 1];
338 buffer[buffer_pos++] = (byte) ch;
345 if (buffer_pos != 0) {
346 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
353 if (buffer_pos != 0) {
354 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
357 return sb.ToString ();
360 public override int GetHashCode ()
362 return dispositionType.ToLowerInvariant ().GetHashCode () ^
363 HashCodeCalculator.Calculate (parameters);
366 public static ContentDispositionHeaderValue Parse (string input)
368 ContentDispositionHeaderValue value;
369 if (TryParse (input, out value))
372 throw new FormatException (input);
375 void SetDateValue (string key, DateTimeOffset? value)
377 SetValue (key, value == null ? null : ("\"" + value.Value.ToString ("r", CultureInfo.InvariantCulture)) + "\"");
380 void SetValue (string key, string value)
382 if (parameters == null)
383 parameters = new List<NameValueHeaderValue> ();
385 parameters.SetValue (key, value);
388 public override string ToString ()
390 return dispositionType + CollectionExtensions.ToString (parameters);
393 public static bool TryParse (string input, out ContentDispositionHeaderValue parsedValue)
397 var lexer = new Lexer (input);
398 var t = lexer.Scan ();
399 if (t.Kind != Token.Type.Token)
402 List<NameValueHeaderValue> parameters = null;
403 var type = lexer.GetStringValue (t);
408 case Token.Type.SeparatorSemicolon:
409 if (!NameValueHeaderValue.TryParseParameters (lexer, out parameters, out t) || t != Token.Type.End)
418 parsedValue = new ContentDispositionHeaderValue () {
419 dispositionType = type,
420 parameters = parameters