//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.ChannelElements {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Text;
using DotNetOpenAuth.Messaging;
///
/// Indicates the level of strictness to require when decoding a
/// Key-Value Form encoded dictionary.
///
public enum KeyValueFormConformanceLevel {
///
/// Be as forgiving as possible to errors made while encoding.
///
Loose,
///
/// Allow for certain errors in encoding attributable to ambiguities
/// in the OpenID 1.1 spec's description of the encoding.
///
OpenId11,
///
/// The strictest mode. The decoder requires the encoded dictionary
/// to be in strict compliance with OpenID 2.0's description of
/// the encoding.
///
OpenId20,
}
///
/// Performs conversion to and from the Key-Value Form Encoding defined by
/// OpenID Authentication 2.0 section 4.1.1.
/// http://openid.net/specs/openid-authentication-2_0.html#anchor4
///
///
/// This class is thread safe and immutable.
///
internal class KeyValueFormEncoding {
///
/// Characters that must not appear in parameter names.
///
private static readonly char[] IllegalKeyCharacters = { '\n', ':' };
///
/// Characters that must not appaer in parameter values.
///
private static readonly char[] IllegalValueCharacters = { '\n' };
///
/// The newline character sequence to use.
///
private const string NewLineCharacters = "\n";
///
/// The character encoding to use.
///
private static readonly Encoding textEncoding = new UTF8Encoding(false);
///
/// Initializes a new instance of the class.
///
public KeyValueFormEncoding() {
this.ConformanceLevel = KeyValueFormConformanceLevel.Loose;
}
///
/// Initializes a new instance of the class.
///
/// How strictly an incoming Key-Value Form message will be held to the spec.
public KeyValueFormEncoding(KeyValueFormConformanceLevel conformanceLevel) {
this.ConformanceLevel = conformanceLevel;
}
///
/// Gets a value controlling how strictly an incoming Key-Value Form message will be held to the spec.
///
public KeyValueFormConformanceLevel ConformanceLevel { get; private set; }
///
/// Encodes key/value pairs to Key-Value Form.
///
///
/// The dictionary of key/value pairs to convert to a byte stream.
///
/// The UTF8 byte array.
///
/// Enumerating a Dictionary<TKey, TValue> has undeterministic ordering.
/// If ordering of the key=value pairs is important, a deterministic enumerator must
/// be used.
///
[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")]
public static byte[] GetBytes(IEnumerable> keysAndValues) {
Requires.NotNull(keysAndValues, "keysAndValues");
using (MemoryStream ms = new MemoryStream()) {
using (StreamWriter sw = new StreamWriter(ms, textEncoding)) {
sw.NewLine = NewLineCharacters;
foreach (var pair in keysAndValues) {
if (pair.Key.IndexOfAny(IllegalKeyCharacters) >= 0) {
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Key));
}
if (pair.Value.IndexOfAny(IllegalValueCharacters) >= 0) {
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Value));
}
sw.Write(pair.Key);
sw.Write(':');
sw.Write(pair.Value);
sw.WriteLine();
}
}
return ms.ToArray();
}
}
///
/// Decodes bytes in Key-Value Form to key/value pairs.
///
/// The stream of Key-Value Form encoded bytes.
/// The deserialized dictionary.
/// Thrown when the data is not in the expected format.
public IDictionary GetDictionary(Stream data) {
using (StreamReader reader = new StreamReader(data, textEncoding)) {
var dict = new Dictionary();
int line_num = 0;
string line;
while ((line = reader.ReadLine()) != null) {
line_num++;
if (this.ConformanceLevel == KeyValueFormConformanceLevel.Loose) {
line = line.Trim();
if (line.Length == 0) {
continue;
}
}
string[] parts = line.Split(new[] { ':' }, 2);
ErrorUtilities.VerifyFormat(parts.Length == 2, OpenIdStrings.InvalidKeyValueFormCharacterMissing, ':', line_num, line);
if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) {
ErrorUtilities.VerifyFormat(!(char.IsWhiteSpace(parts[0], parts[0].Length - 1) || char.IsWhiteSpace(parts[1], 0)), OpenIdStrings.InvalidCharacterInKeyValueFormInput, ' ', line_num, line);
}
if (this.ConformanceLevel < KeyValueFormConformanceLevel.OpenId20) {
parts[0] = parts[0].Trim();
parts[1] = parts[1].Trim();
}
// calling Add method will throw if a key is encountered twice,
// which we should do.
dict.Add(parts[0], parts[1]);
}
if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) {
reader.BaseStream.Seek(-1, SeekOrigin.End);
ErrorUtilities.VerifyFormat(reader.BaseStream.ReadByte() == '\n', OpenIdStrings.InvalidKeyValueFormCharacterMissing, "\\n", line_num, line);
}
return dict;
}
}
}
}