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"));
260 if (!Lexer.IsValidCharacter (ch) || ch == '*' || ch == '?' || ch == '%') {
261 sb.Append (Uri.HexEscape (ch));
268 return sb.ToString ();
271 static string DecodeValue (string value, bool extendedNotation)
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].
282 if (value.Length < 2)
289 if (value[0] == '\"') {
291 // Is Base64 encoded ?
292 // encoded-word := "=?" charset "?" encoding "?" encoded-text "?="
294 sep = value.Split ('?');
295 if (sep.Length != 5 || sep[0] != "\"=" || sep[4] != "=\"" || (sep[2] != "B" && sep[2] != "b"))
299 encoding = Encoding.GetEncoding (sep[1]);
300 return encoding.GetString (Convert.FromBase64String (sep[3]));
306 if (!extendedNotation)
310 // RFC 5987: Charset/Language Encoding
312 sep = value.Split ('\'');
317 encoding = Encoding.GetEncoding (sep[0]);
322 // TODO: What to do with sep[1] language
326 int pct_encoded = value.IndexOf ('%');
330 StringBuilder sb = new StringBuilder ();
331 byte[] buffer = null;
334 for (int i = 0; i < value.Length;) {
338 ch = Uri.HexUnescape (value, ref i);
339 if (ch != unescaped) {
341 buffer = new byte[value.Length - i + 1];
343 buffer[buffer_pos++] = (byte) ch;
350 if (buffer_pos != 0) {
351 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
358 if (buffer_pos != 0) {
359 sb.Append (encoding.GetChars (buffer, 0, buffer_pos));
362 return sb.ToString ();
365 public override int GetHashCode ()
367 return dispositionType.ToLowerInvariant ().GetHashCode () ^
368 HashCodeCalculator.Calculate (parameters);
371 public static ContentDispositionHeaderValue Parse (string input)
373 ContentDispositionHeaderValue value;
374 if (TryParse (input, out value))
377 throw new FormatException (input);
380 void SetDateValue (string key, DateTimeOffset? value)
382 SetValue (key, value == null ? null : ("\"" + value.Value.ToString ("r", CultureInfo.InvariantCulture)) + "\"");
385 void SetValue (string key, string value)
387 if (parameters == null)
388 parameters = new List<NameValueHeaderValue> ();
390 parameters.SetValue (key, value);
393 public override string ToString ()
395 return dispositionType + CollectionExtensions.ToString (parameters);
398 public static bool TryParse (string input, out ContentDispositionHeaderValue parsedValue)
402 var lexer = new Lexer (input);
403 var t = lexer.Scan ();
404 if (t.Kind != Token.Type.Token)
407 List<NameValueHeaderValue> parameters = null;
408 var type = lexer.GetStringValue (t);
413 case Token.Type.SeparatorSemicolon:
414 if (!NameValueHeaderValue.TryParseParameters (lexer, out parameters, out t) || t != Token.Type.End)
423 parsedValue = new ContentDispositionHeaderValue () {
424 dispositionType = type,
425 parameters = parameters