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 return FindParameter ("name");
126 SetValue ("name", value);
130 public ICollection<NameValueHeaderValue> Parameters {
132 return parameters ?? (parameters = new List<NameValueHeaderValue> ());
136 public DateTimeOffset? ReadDate {
138 return GetDateValue ("read-date");
141 SetDateValue ("read-date", value);
147 var found = FindParameter ("size");
149 if (Parser.Long.TryParse (found, out result))
156 SetValue ("size", null);
161 throw new ArgumentOutOfRangeException ("value");
163 SetValue ("size", value.Value.ToString (CultureInfo.InvariantCulture));
167 object ICloneable.Clone ()
169 return new ContentDispositionHeaderValue (this);
172 public override bool Equals (object obj)
174 var source = obj as ContentDispositionHeaderValue;
175 return source != null &&
176 string.Equals (source.dispositionType, dispositionType, StringComparison.OrdinalIgnoreCase) &&
177 source.parameters.SequenceEqual (parameters);
180 string FindParameter (string name)
182 if (parameters == null)
185 foreach (var entry in parameters) {
186 if (string.Equals (entry.Name, name, StringComparison.OrdinalIgnoreCase))
193 DateTimeOffset? GetDateValue (string name)
195 var value = FindParameter (name);
196 if (value == null || value == null)
199 if (value.Length < 3)
202 if (value[0] == '\"')
203 value = value.Substring (1, value.Length - 2);
205 DateTimeOffset offset;
206 if (Lexer.TryGetDateValue (value, out offset))
212 static string EncodeBase64Value (string value)
214 for (int i = 0; i < value.Length; ++i) {
217 var encoding = Encoding.UTF8;
218 return string.Format ("\"=?{0}?B?{1}?=\"",
219 encoding.WebName, Convert.ToBase64String (encoding.GetBytes (value)));
223 if (!Lexer.IsValidToken (value))
224 return "\"" + value + "\"";
229 static string EncodeRFC5987 (string value)
231 var encoding = Encoding.UTF8;
232 StringBuilder sb = new StringBuilder (value.Length + 11);
233 sb.Append (encoding.WebName);
237 for (int i = 0; i < value.Length; ++i) {
240 foreach (var b in encoding.GetBytes (new[] { ch })) {
242 sb.Append (b.ToString ("X2"));
251 return sb.ToString ();
254 static string DecodeValue (string value, bool extendedNotation)
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].
265 if (value.Length < 2)
272 if (value[0] == '\"') {
274 // Is Base64 encoded ?
275 // encoded-word := "=?" charset "?" encoding "?" encoded-text "?="
277 sep = value.Split ('?');
278 if (sep.Length != 5 || sep[0] != "\"=" || sep[4] != "=\"" || (sep[2] != "B" && sep[2] != "b"))
282 encoding = Encoding.GetEncoding (sep[1]);
283 return encoding.GetString (Convert.FromBase64String (sep[3]));
289 if (!extendedNotation)
293 // RFC 5987: Charset/Language Encoding
295 sep = value.Split ('\'');
300 encoding = Encoding.GetEncoding (sep[0]);
305 // TODO: What to do with sep[1] language
309 int pct_encoded = value.IndexOf ('%');
313 StringBuilder sb = new StringBuilder ();
314 byte[] buffer = null;
317 for (int i = 0; i < value.Length;) {
321 ch = Uri.HexUnescape (value, ref i);
322 if (ch != unescaped) {
324 buffer = new byte[value.Length - i + 1];
326 buffer[buffer_pos++] = (byte) ch;
333 if (buffer_pos != 0) {
334 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
341 if (buffer_pos != 0) {
342 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
345 return sb.ToString ();
348 public override int GetHashCode ()
350 return dispositionType.ToLowerInvariant ().GetHashCode () ^
351 HashCodeCalculator.Calculate (parameters);
354 public static ContentDispositionHeaderValue Parse (string input)
356 ContentDispositionHeaderValue value;
357 if (TryParse (input, out value))
360 throw new FormatException (input);
363 void SetDateValue (string key, DateTimeOffset? value)
365 SetValue (key, value == null ? null : ("\"" + value.Value.ToString ("r", CultureInfo.InvariantCulture)) + "\"");
368 void SetValue (string key, string value)
370 if (parameters == null)
371 parameters = new List<NameValueHeaderValue> ();
373 parameters.SetValue (key, value);
376 public override string ToString ()
378 return dispositionType + CollectionExtensions.ToString (parameters);
381 public static bool TryParse (string input, out ContentDispositionHeaderValue parsedValue)
385 var lexer = new Lexer (input);
386 var t = lexer.Scan ();
387 if (t.Kind != Token.Type.Token)
390 List<NameValueHeaderValue> parameters = null;
391 var type = lexer.GetStringValue (t);
396 case Token.Type.SeparatorSemicolon:
397 if (!NameValueHeaderValue.TryParseParameters (lexer, out parameters, out t) || t != Token.Type.End)
406 parsedValue = new ContentDispositionHeaderValue () {
407 dispositionType = type,
408 parameters = parameters