diff --git a/src/MonoTorrent/BEncoding/BEncodedDictionary.cs b/src/MonoTorrent/BEncoding/BEncodedDictionary.cs
new file mode 100644
index 000000000..7efd11392
--- /dev/null
+++ b/src/MonoTorrent/BEncoding/BEncodedDictionary.cs
@@ -0,0 +1,321 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoTorrent.BEncoding
+{
+ ///
+ /// Class representing a BEncoded Dictionary
+ ///
+ public class BEncodedDictionary : BEncodedValue, IDictionary
+ {
+ #region Member Variables
+
+ private SortedDictionary dictionary;
+
+ #endregion
+
+
+ #region Constructors
+
+ ///
+ /// Create a new BEncodedDictionary
+ ///
+ public BEncodedDictionary()
+ {
+ this.dictionary = new SortedDictionary();
+ }
+
+ #endregion
+
+
+ #region Encode/Decode Methods
+
+ ///
+ /// Encodes the dictionary to a byte[]
+ ///
+ /// The buffer to encode the data to
+ /// The offset to start writing the data to
+ ///
+ public override int Encode(byte[] buffer, int offset)
+ {
+ int written = 0;
+
+ //Dictionaries start with 'd'
+ buffer[offset] = (byte)'d';
+ written++;
+
+ foreach (KeyValuePair keypair in this)
+ {
+ written += keypair.Key.Encode(buffer, offset + written);
+ written += keypair.Value.Encode(buffer, offset + written);
+ }
+
+ // Dictionaries end with 'e'
+ buffer[offset + written] = (byte)'e';
+ written++;
+ return written;
+ }
+
+
+ ///
+ ///
+ ///
+ ///
+ internal override void DecodeInternal(RawReader reader)
+ {
+ DecodeInternal(reader, reader.StrictDecoding);
+ }
+
+ private void DecodeInternal(RawReader reader, bool strictDecoding)
+ {
+ BEncodedString key = null;
+ BEncodedValue value = null;
+ BEncodedString oldkey = null;
+
+ if (reader.ReadByte() != 'd')
+ throw new BEncodingException("Invalid data found. Aborting"); // Remove the leading 'd'
+
+ while ((reader.PeekByte() != -1) && (reader.PeekByte() != 'e'))
+ {
+ key = (BEncodedString)BEncodedValue.Decode(reader); // keys have to be BEncoded strings
+
+ if (oldkey != null && oldkey.CompareTo(key) > 0)
+ if (strictDecoding)
+ throw new BEncodingException(String.Format(
+ "Illegal BEncodedDictionary. The attributes are not ordered correctly. Old key: {0}, New key: {1}",
+ oldkey, key));
+
+ oldkey = key;
+ value = BEncodedValue.Decode(reader); // the value is a BEncoded value
+ dictionary.Add(key, value);
+ }
+
+ if (reader.ReadByte() != 'e') // remove the trailing 'e'
+ throw new BEncodingException("Invalid data found. Aborting");
+ }
+
+ public static BEncodedDictionary DecodeTorrent(byte[] bytes)
+ {
+ return DecodeTorrent(new MemoryStream(bytes));
+ }
+
+ public static BEncodedDictionary DecodeTorrent(Stream s)
+ {
+ return DecodeTorrent(new RawReader(s));
+ }
+
+
+ ///
+ /// Special decoding method for torrent files - allows dictionary attributes to be out of order for the
+ /// overall torrent file, but imposes strict rules on the info dictionary.
+ ///
+ ///
+ public static BEncodedDictionary DecodeTorrent(RawReader reader)
+ {
+ BEncodedString key = null;
+ BEncodedValue value = null;
+ BEncodedDictionary torrent = new BEncodedDictionary();
+ if (reader.ReadByte() != 'd')
+ throw new BEncodingException("Invalid data found. Aborting"); // Remove the leading 'd'
+
+ while ((reader.PeekByte() != -1) && (reader.PeekByte() != 'e'))
+ {
+ key = (BEncodedString)BEncodedValue.Decode(reader); // keys have to be BEncoded strings
+
+ if (reader.PeekByte() == 'd')
+ {
+ value = new BEncodedDictionary();
+ if (key.Text.ToLower().Equals("info"))
+ ((BEncodedDictionary)value).DecodeInternal(reader, true);
+ else
+ ((BEncodedDictionary)value).DecodeInternal(reader, false);
+ }
+ else
+ value = BEncodedValue.Decode(reader); // the value is a BEncoded value
+
+ torrent.dictionary.Add(key, value);
+ }
+
+ if (reader.ReadByte() != 'e') // remove the trailing 'e'
+ throw new BEncodingException("Invalid data found. Aborting");
+
+ return torrent;
+ }
+
+ #endregion
+
+
+ #region Helper Methods
+
+ ///
+ /// Returns the size of the dictionary in bytes using UTF8 encoding
+ ///
+ ///
+ public override int LengthInBytes()
+ {
+ int length = 0;
+ length += 1; // Dictionaries start with 'd'
+
+ foreach (KeyValuePair keypair in this.dictionary)
+ {
+ length += keypair.Key.LengthInBytes();
+ length += keypair.Value.LengthInBytes();
+ }
+ length += 1; // Dictionaries end with 'e'
+ return length;
+ }
+
+ #endregion
+
+
+ #region Overridden Methods
+ public override bool Equals(object obj)
+ {
+ BEncodedValue val;
+ BEncodedDictionary other = obj as BEncodedDictionary;
+ if (other == null)
+ return false;
+
+ if (this.dictionary.Count != other.dictionary.Count)
+ return false;
+
+ foreach (KeyValuePair keypair in this.dictionary)
+ {
+ if (!other.TryGetValue(keypair.Key, out val))
+ return false;
+
+ if (!keypair.Value.Equals(val))
+ return false;
+ }
+
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ int result = 0;
+ foreach (KeyValuePair keypair in dictionary)
+ {
+ result ^= keypair.Key.GetHashCode();
+ result ^= keypair.Value.GetHashCode();
+ }
+
+ return result;
+ }
+
+ public override string ToString()
+ {
+ return System.Text.Encoding.UTF8.GetString(Encode());
+ }
+ #endregion
+
+
+ #region IDictionary and IList methods
+ public void Add(BEncodedString key, BEncodedValue value)
+ {
+ this.dictionary.Add(key, value);
+ }
+
+ public void Add(KeyValuePair item)
+ {
+ this.dictionary.Add(item.Key, item.Value);
+ }
+ public void Clear()
+ {
+ this.dictionary.Clear();
+ }
+
+ public bool Contains(KeyValuePair item)
+ {
+ if (!this.dictionary.ContainsKey(item.Key))
+ return false;
+
+ return this.dictionary[item.Key].Equals(item.Value);
+ }
+
+ public bool ContainsKey(BEncodedString key)
+ {
+ return this.dictionary.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ this.dictionary.CopyTo(array, arrayIndex);
+ }
+
+ public int Count
+ {
+ get { return this.dictionary.Count; }
+ }
+
+ //public int IndexOf(KeyValuePair item)
+ //{
+ // return this.dictionary.IndexOf(item);
+ //}
+
+ //public void Insert(int index, KeyValuePair item)
+ //{
+ // this.dictionary.Insert(index, item);
+ //}
+
+ public bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public bool Remove(BEncodedString key)
+ {
+ return this.dictionary.Remove(key);
+ }
+
+ public bool Remove(KeyValuePair item)
+ {
+ return this.dictionary.Remove(item.Key);
+ }
+
+ //public void RemoveAt(int index)
+ //{
+ // this.dictionary.RemoveAt(index);
+ //}
+
+ public bool TryGetValue(BEncodedString key, out BEncodedValue value)
+ {
+ return this.dictionary.TryGetValue(key, out value);
+ }
+
+ public BEncodedValue this[BEncodedString key]
+ {
+ get { return this.dictionary[key]; }
+ set { this.dictionary[key] = value; }
+ }
+
+ //public KeyValuePair this[int index]
+ //{
+ // get { return this.dictionary[index]; }
+ // set { this.dictionary[index] = value; }
+ //}
+
+ public ICollection Keys
+ {
+ get { return this.dictionary.Keys; }
+ }
+
+ public ICollection Values
+ {
+ get { return this.dictionary.Values; }
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return this.dictionary.GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return this.dictionary.GetEnumerator();
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/MonoTorrent/BEncoding/BEncodedList.cs b/src/MonoTorrent/BEncoding/BEncodedList.cs
new file mode 100644
index 000000000..5dda07fe8
--- /dev/null
+++ b/src/MonoTorrent/BEncoding/BEncodedList.cs
@@ -0,0 +1,219 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+
+namespace MonoTorrent.BEncoding
+{
+ ///
+ /// Class representing a BEncoded list
+ ///
+ public class BEncodedList : BEncodedValue, IList
+ {
+ #region Member Variables
+
+ private List list;
+
+ #endregion
+
+
+ #region Constructors
+ ///
+ /// Create a new BEncoded List with default capacity
+ ///
+ public BEncodedList()
+ : this(new List())
+ {
+ }
+
+ ///
+ /// Create a new BEncoded List with the supplied capacity
+ ///
+ /// The initial capacity
+ public BEncodedList(int capacity)
+ : this(new List(capacity))
+ {
+
+ }
+
+ public BEncodedList(IEnumerable list)
+ {
+ if (list == null)
+ throw new ArgumentNullException("list");
+
+ this.list = new List(list);
+ }
+
+ private BEncodedList(List value)
+ {
+ this.list = value;
+ }
+
+ #endregion
+
+
+ #region Encode/Decode Methods
+
+
+ ///
+ /// Encodes the list to a byte[]
+ ///
+ /// The buffer to encode the list to
+ /// The offset to start writing the data at
+ ///
+ public override int Encode(byte[] buffer, int offset)
+ {
+ int written = 0;
+ buffer[offset] = (byte)'l';
+ written++;
+ for (int i = 0; i < this.list.Count; i++)
+ written += this.list[i].Encode(buffer, offset + written);
+ buffer[offset + written] = (byte)'e';
+ written++;
+ return written;
+ }
+
+ ///
+ /// Decodes a BEncodedList from the given StreamReader
+ ///
+ ///
+ internal override void DecodeInternal(RawReader reader)
+ {
+ if (reader.ReadByte() != 'l') // Remove the leading 'l'
+ throw new BEncodingException("Invalid data found. Aborting");
+
+ while ((reader.PeekByte() != -1) && (reader.PeekByte() != 'e'))
+ list.Add(BEncodedValue.Decode(reader));
+
+ if (reader.ReadByte() != 'e') // Remove the trailing 'e'
+ throw new BEncodingException("Invalid data found. Aborting");
+ }
+ #endregion
+
+
+ #region Helper Methods
+ ///
+ /// Returns the size of the list in bytes
+ ///
+ ///
+ public override int LengthInBytes()
+ {
+ int length = 0;
+
+ length += 1; // Lists start with 'l'
+ for (int i=0; i < this.list.Count; i++)
+ length += this.list[i].LengthInBytes();
+
+ length += 1; // Lists end with 'e'
+ return length;
+ }
+ #endregion
+
+
+ #region Overridden Methods
+ public override bool Equals(object obj)
+ {
+ BEncodedList other = obj as BEncodedList;
+
+ if (other == null)
+ return false;
+
+ for (int i = 0; i < this.list.Count; i++)
+ if (!this.list[i].Equals(other.list[i]))
+ return false;
+
+ return true;
+ }
+
+
+ public override int GetHashCode()
+ {
+ int result = 0;
+ for (int i = 0; i < list.Count; i++)
+ result ^= list[i].GetHashCode();
+
+ return result;
+ }
+
+
+ public override string ToString()
+ {
+ return System.Text.Encoding.UTF8.GetString(Encode());
+ }
+ #endregion
+
+
+ #region IList methods
+ public void Add(BEncodedValue item)
+ {
+ this.list.Add(item);
+ }
+
+ public void AddRange (IEnumerable collection)
+ {
+ list.AddRange (collection);
+ }
+
+ public void Clear()
+ {
+ this.list.Clear();
+ }
+
+ public bool Contains(BEncodedValue item)
+ {
+ return this.list.Contains(item);
+ }
+
+ public void CopyTo(BEncodedValue[] array, int arrayIndex)
+ {
+ this.list.CopyTo(array, arrayIndex);
+ }
+
+ public int Count
+ {
+ get { return this.list.Count; }
+ }
+
+ public int IndexOf(BEncodedValue item)
+ {
+ return this.list.IndexOf(item);
+ }
+
+ public void Insert(int index, BEncodedValue item)
+ {
+ this.list.Insert(index, item);
+ }
+
+ public bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public bool Remove(BEncodedValue item)
+ {
+ return this.list.Remove(item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ this.list.RemoveAt(index);
+ }
+
+ public BEncodedValue this[int index]
+ {
+ get { return this.list[index]; }
+ set { this.list[index] = value; }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return this.list.GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/MonoTorrent/BEncoding/BEncodedNumber.cs b/src/MonoTorrent/BEncoding/BEncodedNumber.cs
new file mode 100644
index 000000000..9f227399c
--- /dev/null
+++ b/src/MonoTorrent/BEncoding/BEncodedNumber.cs
@@ -0,0 +1,209 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Collections.Generic;
+
+namespace MonoTorrent.BEncoding
+{
+ ///
+ /// Class representing a BEncoded number
+ ///
+ public class BEncodedNumber : BEncodedValue, IComparable
+ {
+ #region Member Variables
+ ///
+ /// The value of the BEncodedNumber
+ ///
+ public long Number
+ {
+ get { return number; }
+ set { number = value; }
+ }
+ internal long number;
+ #endregion
+
+
+ #region Constructors
+ public BEncodedNumber()
+ : this(0)
+ {
+ }
+
+ ///
+ /// Create a new BEncoded number with the given value
+ ///
+ /// The inital value of the BEncodedNumber
+ public BEncodedNumber(long value)
+ {
+ this.number = value;
+ }
+
+ public static implicit operator BEncodedNumber(long value)
+ {
+ return new BEncodedNumber(value);
+ }
+ #endregion
+
+
+ #region Encode/Decode Methods
+
+ ///
+ /// Encodes this number to the supplied byte[] starting at the supplied offset
+ ///
+ /// The buffer to write the data to
+ /// The offset to start writing the data at
+ ///
+ public override int Encode(byte[] buffer, int offset)
+ {
+ long number = this.number;
+
+ int written = offset;
+ buffer[written++] = (byte)'i';
+
+ if (number < 0)
+ {
+ buffer[written++] = (byte)'-';
+ number = -number;
+ }
+ // Reverse the number '12345' to get '54321'
+ long reversed = 0;
+ for (long i = number; i != 0; i /= 10)
+ reversed = reversed * 10 + i % 10;
+
+ // Write each digit of the reversed number to the array. We write '1'
+ // first, then '2', etc
+ for (long i = reversed; i != 0; i /= 10)
+ buffer[written++] = (byte)(i % 10 + '0');
+
+ if (number == 0)
+ buffer[written++] = (byte)'0';
+
+ // If the original number ends in one or more zeros, they are lost
+ // when we reverse the number. We add them back in here.
+ for (long i = number; i % 10 == 0 && number != 0; i /= 10)
+ buffer[written++] = (byte)'0';
+
+ buffer[written++] = (byte)'e';
+ return written - offset;
+ }
+
+
+ ///
+ /// Decodes a BEncoded number from the supplied RawReader
+ ///
+ /// RawReader containing a BEncoded Number
+ internal override void DecodeInternal(RawReader reader)
+ {
+ int sign = 1;
+ if (reader == null)
+ throw new ArgumentNullException("reader");
+
+ if (reader.ReadByte() != 'i') // remove the leading 'i'
+ throw new BEncodingException("Invalid data found. Aborting.");
+
+ if (reader.PeekByte() == '-')
+ {
+ sign = -1;
+ reader.ReadByte ();
+ }
+
+ int letter;
+ while (((letter = reader.PeekByte()) != -1) && letter != 'e')
+ {
+ if(letter < '0' || letter > '9')
+ throw new BEncodingException("Invalid number found.");
+ number = number * 10 + (letter - '0');
+ reader.ReadByte ();
+ }
+ if (reader.ReadByte() != 'e') //remove the trailing 'e'
+ throw new BEncodingException("Invalid data found. Aborting.");
+
+ number *= sign;
+ }
+ #endregion
+
+
+ #region Helper Methods
+ ///
+ /// Returns the length of the encoded string in bytes
+ ///
+ ///
+ public override int LengthInBytes()
+ {
+ long number = this.number;
+ int count = 2; // account for the 'i' and 'e'
+
+ if (number == 0)
+ return count + 1;
+
+ if (number < 0)
+ {
+ number = -number;
+ count++;
+ }
+ for (long i = number; i != 0; i /= 10)
+ count++;
+
+ return count;
+ }
+
+
+ public int CompareTo(object other)
+ {
+ if (other is BEncodedNumber || other is long || other is int)
+ return CompareTo((BEncodedNumber)other);
+
+ return -1;
+ }
+
+ public int CompareTo(BEncodedNumber other)
+ {
+ if (other == null)
+ throw new ArgumentNullException("other");
+
+ return this.number.CompareTo(other.number);
+ }
+
+
+ public int CompareTo(long other)
+ {
+ return this.number.CompareTo(other);
+ }
+ #endregion
+
+
+ #region Overridden Methods
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override bool Equals(object obj)
+ {
+ BEncodedNumber obj2 = obj as BEncodedNumber;
+ if (obj2 == null)
+ return false;
+
+ return (this.number == obj2.number);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override int GetHashCode()
+ {
+ return this.number.GetHashCode();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override string ToString()
+ {
+ return (this.number.ToString());
+ }
+ #endregion
+ }
+}
diff --git a/src/MonoTorrent/BEncoding/BEncodedString.cs b/src/MonoTorrent/BEncoding/BEncodedString.cs
new file mode 100644
index 000000000..e4e65fb10
--- /dev/null
+++ b/src/MonoTorrent/BEncoding/BEncodedString.cs
@@ -0,0 +1,220 @@
+using System;
+using System.IO;
+using System.Collections;
+using System.Text;
+using MonoTorrent.Common;
+using MonoTorrent.Messages;
+
+namespace MonoTorrent.BEncoding
+{
+ ///
+ /// Class representing a BEncoded string
+ ///
+ public class BEncodedString : BEncodedValue, IComparable
+ {
+ #region Member Variables
+
+ ///
+ /// The value of the BEncodedString
+ ///
+ public string Text
+ {
+ get { return Encoding.UTF8.GetString(textBytes); }
+ set { textBytes = Encoding.UTF8.GetBytes(value); }
+ }
+
+ ///
+ /// The underlying byte[] associated with this BEncodedString
+ ///
+ public byte[] TextBytes
+ {
+ get { return this.textBytes; }
+ }
+ private byte[] textBytes;
+ #endregion
+
+
+ #region Constructors
+ ///
+ /// Create a new BEncodedString using UTF8 encoding
+ ///
+ public BEncodedString()
+ : this(new byte[0])
+ {
+ }
+
+ ///
+ /// Create a new BEncodedString using UTF8 encoding
+ ///
+ ///
+ public BEncodedString(char[] value)
+ : this(System.Text.Encoding.UTF8.GetBytes(value))
+ {
+ }
+
+ ///
+ /// Create a new BEncodedString using UTF8 encoding
+ ///
+ /// Initial value for the string
+ public BEncodedString(string value)
+ : this(System.Text.Encoding.UTF8.GetBytes(value))
+ {
+ }
+
+
+ ///
+ /// Create a new BEncodedString using UTF8 encoding
+ ///
+ ///
+ public BEncodedString(byte[] value)
+ {
+ this.textBytes = value;
+ }
+
+
+ public static implicit operator BEncodedString(string value)
+ {
+ return new BEncodedString(value);
+ }
+ public static implicit operator BEncodedString(char[] value)
+ {
+ return new BEncodedString(value);
+ }
+ public static implicit operator BEncodedString(byte[] value)
+ {
+ return new BEncodedString(value);
+ }
+ #endregion
+
+
+ #region Encode/Decode Methods
+
+
+ ///
+ /// Encodes the BEncodedString to a byte[] using the supplied Encoding
+ ///
+ /// The buffer to encode the string to
+ /// The offset at which to save the data to
+ /// The encoding to use
+ /// The number of bytes encoded
+ public override int Encode(byte[] buffer, int offset)
+ {
+ int written = offset;
+ written += Message.WriteAscii(buffer, written, textBytes.Length.ToString ());
+ written += Message.WriteAscii(buffer, written, ":");
+ written += Message.Write(buffer, written, textBytes);
+ return written - offset;
+ }
+
+
+ ///
+ /// Decodes a BEncodedString from the supplied StreamReader
+ ///
+ /// The StreamReader containing the BEncodedString
+ internal override void DecodeInternal(RawReader reader)
+ {
+ if (reader == null)
+ throw new ArgumentNullException("reader");
+
+ int letterCount;
+ string length = string.Empty;
+
+ while ((reader.PeekByte() != -1) && (reader.PeekByte() != ':')) // read in how many characters
+ length += (char)reader.ReadByte(); // the string is
+
+ if (reader.ReadByte() != ':') // remove the ':'
+ throw new BEncodingException("Invalid data found. Aborting");
+
+ if (!int.TryParse(length, out letterCount))
+ throw new BEncodingException(string.Format("Invalid BEncodedString. Length was '{0}' instead of a number", length));
+
+ this.textBytes = new byte[letterCount];
+ if (reader.Read(textBytes, 0, letterCount) != letterCount)
+ throw new BEncodingException("Couldn't decode string");
+ }
+ #endregion
+
+
+ #region Helper Methods
+ public string Hex
+ {
+ get { return BitConverter.ToString(TextBytes); }
+ }
+
+ public override int LengthInBytes()
+ {
+ // The length is equal to the length-prefix + ':' + length of data
+ int prefix = 1; // Account for ':'
+
+ // Count the number of characters needed for the length prefix
+ for (int i = textBytes.Length; i != 0; i = i/10)
+ prefix += 1;
+
+ if (textBytes.Length == 0)
+ prefix++;
+
+ return prefix + textBytes.Length;
+ }
+
+ public int CompareTo(object other)
+ {
+ return CompareTo(other as BEncodedString);
+ }
+
+
+ public int CompareTo(BEncodedString other)
+ {
+ if (other == null)
+ return 1;
+
+ int difference=0;
+ int length = this.textBytes.Length > other.textBytes.Length ? other.textBytes.Length : this.textBytes.Length;
+
+ for (int i = 0; i < length; i++)
+ if ((difference = this.textBytes[i].CompareTo(other.textBytes[i])) != 0)
+ return difference;
+
+ if (this.textBytes.Length == other.textBytes.Length)
+ return 0;
+
+ return this.textBytes.Length > other.textBytes.Length ? 1 : -1;
+ }
+
+ #endregion
+
+
+ #region Overridden Methods
+
+ public override bool Equals(object obj)
+ {
+ if (obj == null)
+ return false;
+
+ BEncodedString other;
+ if (obj is string)
+ other = new BEncodedString((string)obj);
+ else if (obj is BEncodedString)
+ other = (BEncodedString)obj;
+ else
+ return false;
+
+ return Toolbox.ByteMatch(this.textBytes, other.textBytes);
+ }
+
+ public override int GetHashCode()
+ {
+ int hash = 0;
+ for (int i = 0; i < this.textBytes.Length; i++)
+ hash += this.textBytes[i];
+
+ return hash;
+ }
+
+ public override string ToString()
+ {
+ return System.Text.Encoding.UTF8.GetString(textBytes);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/MonoTorrent/BEncoding/BEncodingException.cs b/src/MonoTorrent/BEncoding/BEncodingException.cs
new file mode 100644
index 000000000..6e95b9ecf
--- /dev/null
+++ b/src/MonoTorrent/BEncoding/BEncodingException.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Text;
+using System.Runtime.Serialization;
+
+namespace MonoTorrent.BEncoding
+{
+ [Serializable]
+ public class BEncodingException : Exception
+ {
+ public BEncodingException()
+ : base()
+ {
+ }
+
+ public BEncodingException(string message)
+ : base(message)
+ {
+ }
+
+ public BEncodingException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ protected BEncodingException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/src/MonoTorrent/BEncoding/IBEncodedValue.cs b/src/MonoTorrent/BEncoding/IBEncodedValue.cs
new file mode 100644
index 000000000..0097e0e50
--- /dev/null
+++ b/src/MonoTorrent/BEncoding/IBEncodedValue.cs
@@ -0,0 +1,203 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace MonoTorrent.BEncoding
+{
+ ///
+ /// Base interface for all BEncoded values.
+ ///
+ public abstract class BEncodedValue
+ {
+ internal abstract void DecodeInternal(RawReader reader);
+
+ ///
+ /// Encodes the BEncodedValue into a byte array
+ ///
+ /// Byte array containing the BEncoded Data
+ public byte[] Encode()
+ {
+ byte[] buffer = new byte[LengthInBytes()];
+ if (Encode(buffer, 0) != buffer.Length)
+ throw new BEncodingException("Error encoding the data");
+
+ return buffer;
+ }
+
+
+ ///
+ /// Encodes the BEncodedValue into the supplied buffer
+ ///
+ /// The buffer to encode the information to
+ /// The offset in the buffer to start writing the data
+ ///
+ public abstract int Encode(byte[] buffer, int offset);
+
+ public static T Clone (T value)
+ where T : BEncodedValue
+ {
+ Check.Value (value);
+ return (T) BEncodedValue.Decode (value.Encode ());
+ }
+
+ ///
+ /// Interface for all BEncoded values
+ ///
+ /// The byte array containing the BEncoded data
+ ///
+ public static BEncodedValue Decode(byte[] data)
+ {
+ if (data == null)
+ throw new ArgumentNullException("data");
+
+ using (RawReader stream = new RawReader(new MemoryStream(data)))
+ return (Decode(stream));
+ }
+
+ internal static BEncodedValue Decode(byte[] buffer, bool strictDecoding)
+ {
+ return Decode(buffer, 0, buffer.Length, strictDecoding);
+ }
+
+ ///
+ /// Decode BEncoded data in the given byte array
+ ///
+ /// The byte array containing the BEncoded data
+ /// The offset at which the data starts at
+ /// The number of bytes to be decoded
+ /// BEncodedValue containing the data that was in the byte[]
+ public static BEncodedValue Decode(byte[] buffer, int offset, int length)
+ {
+ return Decode(buffer, offset, length, true);
+ }
+
+ public static BEncodedValue Decode(byte[] buffer, int offset, int length, bool strictDecoding)
+ {
+ if (buffer == null)
+ throw new ArgumentNullException("buffer");
+
+ if (offset < 0 || length < 0)
+ throw new IndexOutOfRangeException("Neither offset or length can be less than zero");
+
+ if (offset > buffer.Length - length)
+ throw new ArgumentOutOfRangeException("length");
+
+ using (RawReader reader = new RawReader(new MemoryStream(buffer, offset, length), strictDecoding))
+ return (BEncodedValue.Decode(reader));
+ }
+
+
+ ///
+ /// Decode BEncoded data in the given stream
+ ///
+ /// The stream containing the BEncoded data
+ /// BEncodedValue containing the data that was in the stream
+ public static BEncodedValue Decode(Stream stream)
+ {
+ if (stream == null)
+ throw new ArgumentNullException("stream");
+
+ return Decode(new RawReader(stream));
+ }
+
+
+ ///
+ /// Decode BEncoded data in the given RawReader
+ ///
+ /// The RawReader containing the BEncoded data
+ /// BEncodedValue containing the data that was in the stream
+ public static BEncodedValue Decode(RawReader reader)
+ {
+ if (reader == null)
+ throw new ArgumentNullException("reader");
+
+ BEncodedValue data;
+ switch (reader.PeekByte())
+ {
+ case ('i'): // Integer
+ data = new BEncodedNumber();
+ break;
+
+ case ('d'): // Dictionary
+ data = new BEncodedDictionary();
+ break;
+
+ case ('l'): // List
+ data = new BEncodedList();
+ break;
+
+ case ('1'): // String
+ case ('2'):
+ case ('3'):
+ case ('4'):
+ case ('5'):
+ case ('6'):
+ case ('7'):
+ case ('8'):
+ case ('9'):
+ case ('0'):
+ data = new BEncodedString();
+ break;
+
+ default:
+ throw new BEncodingException("Could not find what value to decode");
+ }
+
+ data.DecodeInternal(reader);
+ return data;
+ }
+
+
+ ///
+ /// Interface for all BEncoded values
+ ///
+ /// The byte array containing the BEncoded data
+ ///
+ public static T Decode(byte[] data) where T : BEncodedValue
+ {
+ return (T)BEncodedValue.Decode(data);
+ }
+
+
+ ///
+ /// Decode BEncoded data in the given byte array
+ ///
+ /// The byte array containing the BEncoded data
+ /// The offset at which the data starts at
+ /// The number of bytes to be decoded
+ /// BEncodedValue containing the data that was in the byte[]
+ public static T Decode(byte[] buffer, int offset, int length) where T : BEncodedValue
+ {
+ return BEncodedValue.Decode(buffer, offset, length, true);
+ }
+
+ public static T Decode(byte[] buffer, int offset, int length, bool strictDecoding) where T : BEncodedValue
+ {
+ return (T)BEncodedValue.Decode(buffer, offset, length, strictDecoding);
+ }
+
+
+ ///
+ /// Decode BEncoded data in the given stream
+ ///
+ /// The stream containing the BEncoded data
+ /// BEncodedValue containing the data that was in the stream
+ public static T Decode(Stream stream) where T : BEncodedValue
+ {
+ return (T)BEncodedValue.Decode(stream);
+ }
+
+
+ public static T Decode(RawReader reader) where T : BEncodedValue
+ {
+ return (T)BEncodedValue.Decode(reader);
+ }
+
+
+ ///
+ /// Returns the size of the byte[] needed to encode this BEncodedValue
+ ///
+ ///
+ public abstract int LengthInBytes();
+ }
+}
diff --git a/src/MonoTorrent/BEncoding/RawReader.cs b/src/MonoTorrent/BEncoding/RawReader.cs
new file mode 100644
index 000000000..47cc983f6
--- /dev/null
+++ b/src/MonoTorrent/BEncoding/RawReader.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+
+namespace MonoTorrent.BEncoding
+{
+ public class RawReader : Stream
+ {
+ bool hasPeek;
+ Stream input;
+ byte[] peeked;
+ bool strictDecoding;
+
+ public bool StrictDecoding
+ {
+ get { return strictDecoding; }
+ }
+
+ public RawReader(Stream input)
+ : this(input, true)
+ {
+
+ }
+
+ public RawReader(Stream input, bool strictDecoding)
+ {
+ this.input = input;
+ this.peeked = new byte[1];
+ this.strictDecoding = strictDecoding;
+ }
+
+ public override bool CanRead
+ {
+ get { return input.CanRead; }
+ }
+
+ public override bool CanSeek
+ {
+ get { return input.CanSeek; }
+ }
+
+ public override bool CanWrite
+ {
+ get { return false; }
+ }
+
+ public override void Flush()
+ {
+ throw new NotSupportedException();
+ }
+
+ public override long Length
+ {
+ get { return input.Length; }
+ }
+
+ public int PeekByte()
+ {
+ if (!hasPeek)
+ hasPeek = Read(peeked, 0, 1) == 1;
+ return hasPeek ? peeked[0] : -1;
+ }
+
+ public override int ReadByte()
+ {
+ if (hasPeek)
+ {
+ hasPeek = false;
+ return peeked[0];
+ }
+ return base.ReadByte();
+ }
+
+ public override long Position
+ {
+ get
+ {
+ if (hasPeek)
+ return input.Position - 1;
+ return input.Position;
+ }
+ set
+ {
+ if (value != Position)
+ {
+ hasPeek = false;
+ input.Position = value;
+ }
+ }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ int read = 0;
+ if (hasPeek && count > 0)
+ {
+ hasPeek = false;
+ buffer[offset] = peeked[0];
+ offset++;
+ count--;
+ read++;
+ }
+ read += input.Read(buffer, offset, count);
+ return read;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ long val;
+ if (hasPeek && origin == SeekOrigin.Current)
+ val = input.Seek(offset - 1, origin);
+ else
+ val = input.Seek(offset, origin);
+ hasPeek = false;
+ return val;
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/src/MonoTorrent/BitField.cs b/src/MonoTorrent/BitField.cs
new file mode 100644
index 000000000..dc23bc626
--- /dev/null
+++ b/src/MonoTorrent/BitField.cs
@@ -0,0 +1,420 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoTorrent
+{
+ ///
+ /// This class is for represting the Peer's bitfield
+ ///
+ public class BitField : ICloneable, IEnumerable
+ {
+ #region Member Variables
+
+ private int[] array;
+ private int length;
+ private int trueCount;
+
+ internal bool AllFalse
+ {
+ get { return this.trueCount == 0; }
+ }
+
+ internal bool AllTrue
+ {
+ get { return this.trueCount == this.length; }
+ }
+
+ public int Length
+ {
+ get { return this.length; }
+ }
+
+ public double PercentComplete
+ {
+ get { return (double)this.trueCount / this.length * 100.0; }
+ }
+
+ #endregion
+
+
+ #region Constructors
+ public BitField(byte[] array, int length)
+ : this(length)
+ {
+ this.FromArray(array, 0, array.Length);
+ }
+
+ public BitField(int length)
+ {
+ if (length < 0)
+ throw new ArgumentOutOfRangeException("length");
+
+ this.length = length;
+ this.array = new int[(length + 31) / 32];
+ }
+
+ public BitField(bool[] array)
+ {
+ this.length = array.Length;
+ this.array = new int[(array.Length + 31) / 32];
+ for (int i = 0; i < array.Length; i++)
+ this.Set(i, array[i]);
+ }
+
+ #endregion
+
+
+ #region Methods BitArray
+
+ public bool this[int index]
+ {
+ get { return this.Get(index); }
+ internal set { this.Set(index, value); }
+ }
+
+ object ICloneable.Clone()
+ {
+ return this.Clone();
+ }
+
+ public BitField Clone()
+ {
+ BitField b = new BitField(this.length);
+ Buffer.BlockCopy(this.array, 0, b.array, 0, this.array.Length * 4);
+ b.trueCount = this.trueCount;
+ return b;
+ }
+
+ public BitField From(BitField value)
+ {
+ this.Check(value);
+ Buffer.BlockCopy(value.array, 0, this.array, 0, this.array.Length * 4);
+ this.trueCount = value.trueCount;
+ return this;
+ }
+
+ public BitField Not()
+ {
+ for (int i = 0; i < this.array.Length; i++)
+ this.array[i] = ~this.array[i];
+
+ this.trueCount = this.length - this.trueCount;
+ return this;
+ }
+
+ public BitField And(BitField value)
+ {
+ this.Check(value);
+
+ for (int i = 0; i < this.array.Length; i++)
+ this.array[i] &= value.array[i];
+
+ this.Validate();
+ return this;
+ }
+
+ internal BitField NAnd(BitField value)
+ {
+ this.Check(value);
+
+ for (int i = 0; i < this.array.Length; i++)
+ this.array[i] &= ~value.array[i];
+
+ this.Validate();
+ return this;
+ }
+
+ public BitField Or(BitField value)
+ {
+ this.Check(value);
+
+ for (int i = 0; i < this.array.Length; i++)
+ this.array[i] |= value.array[i];
+
+ this.Validate();
+ return this;
+ }
+
+ public BitField Xor(BitField value)
+ {
+ this.Check(value);
+
+ for (int i = 0; i < this.array.Length; i++)
+ this.array[i] ^= value.array[i];
+
+ this.Validate();
+ return this;
+ }
+
+ public override bool Equals(object obj)
+ {
+ BitField bf = obj as BitField;
+
+ if (bf == null || this.array.Length != bf.array.Length || this.TrueCount != bf.TrueCount)
+ return false;
+
+ for (int i = 0; i < this.array.Length; i++)
+ if (this.array[i] != bf.array[i])
+ return false;
+
+ return true;
+ }
+
+ public int FirstTrue()
+ {
+ return this.FirstTrue(0, this.length);
+ }
+
+ public int FirstTrue(int startIndex, int endIndex)
+ {
+ int start;
+ int end;
+
+ // If the number of pieces is an exact multiple of 32, we need to decrement by 1 so we don't overrun the array
+ // For the case when endIndex == 0, we need to ensure we don't go negative
+ int loopEnd = Math.Min((endIndex / 32), this.array.Length - 1);
+ for (int i = (startIndex / 32); i <= loopEnd; i++)
+ {
+ if (this.array[i] == 0) // This one has no true values
+ continue;
+
+ start = i * 32;
+ end = start + 32;
+ start = (start < startIndex) ? startIndex : start;
+ end = (end > this.length) ? this.length : end;
+ end = (end > endIndex) ? endIndex : end;
+ if (end == this.Length && end > 0)
+ end--;
+
+ for (int j = start; j <= end; j++)
+ if (this.Get(j)) // This piece is true
+ return j;
+ }
+
+ return -1; // Nothing is true
+ }
+
+ public int FirstFalse()
+ {
+ return this.FirstFalse(0, this.Length);
+ }
+
+ public int FirstFalse(int startIndex, int endIndex)
+ {
+ int start;
+ int end;
+
+ // If the number of pieces is an exact multiple of 32, we need to decrement by 1 so we don't overrun the array
+ // For the case when endIndex == 0, we need to ensure we don't go negative
+ int loopEnd = Math.Min((endIndex / 32), this.array.Length - 1);
+ for (int i = (startIndex / 32); i <= loopEnd; i++)
+ {
+ if (this.array[i] == ~0) // This one has no false values
+ continue;
+
+ start = i * 32;
+ end = start + 32;
+ start = (start < startIndex) ? startIndex : start;
+ end = (end > this.length) ? this.length : end;
+ end = (end > endIndex) ? endIndex : end;
+ if (end == this.Length && end > 0)
+ end--;
+
+ for (int j = start; j <= end; j++)
+ if (!this.Get(j)) // This piece is true
+ return j;
+ }
+
+ return -1; // Nothing is true
+ }
+ internal void FromArray(byte[] buffer, int offset, int length)
+ {
+ int end = this.Length / 32;
+ for (int i = 0; i < end; i++)
+ this.array[i] = (buffer[offset++] << 24) |
+ (buffer[offset++] << 16) |
+ (buffer[offset++] << 8) |
+ (buffer[offset++] << 0);
+
+ int shift = 24;
+ for (int i = end * 32; i < this.Length; i += 8)
+ {
+ this.array[this.array.Length - 1] |= buffer[offset++] << shift;
+ shift -= 8;
+ }
+ this.Validate();
+ }
+
+ bool Get(int index)
+ {
+ if (index < 0 || index >= this.length)
+ throw new ArgumentOutOfRangeException("index");
+
+ return (this.array[index >> 5] & (1 << (31 - (index & 31)))) != 0;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ for (int i = 0; i < this.length; i++)
+ yield return this.Get(i);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ public override int GetHashCode()
+ {
+ int count = 0;
+ for (int i = 0; i < this.array.Length; i++)
+ count += this.array[i];
+
+ return count;
+ }
+
+ public int LengthInBytes
+ {
+ get { return (this.length + 7) / 8; } //8 bits in a byte.
+ }
+
+ public BitField Set(int index, bool value)
+ {
+ if (index < 0 || index >= this.length)
+ throw new ArgumentOutOfRangeException("index");
+
+ if (value)
+ {
+ if ((this.array[index >> 5] & (1 << (31 - (index & 31)))) == 0)// If it's not already true
+ this.trueCount++; // Increase true count
+ this.array[index >> 5] |= (1 << (31 - index & 31));
+ }
+ else
+ {
+ if ((this.array[index >> 5] & (1 << (31 - (index & 31)))) != 0)// If it's not already false
+ this.trueCount--; // Decrease true count
+ this.array[index >> 5] &= ~(1 << (31 - (index & 31)));
+ }
+
+ return this;
+ }
+
+ internal BitField SetTrue(params int[] indices)
+ {
+ foreach (int index in indices)
+ this.Set(index, true);
+ return this;
+ }
+
+ internal BitField SetFalse(params int[] indices)
+ {
+ foreach (int index in indices)
+ this.Set(index, false);
+ return this;
+ }
+
+ internal BitField SetAll(bool value)
+ {
+ if (value)
+ {
+ for (int i = 0; i < this.array.Length; i++)
+ this.array[i] = ~0;
+ this.Validate();
+ }
+
+ else
+ {
+ for (int i = 0; i < this.array.Length; i++)
+ this.array[i] = 0;
+ this.trueCount = 0;
+ }
+
+ return this;
+ }
+
+ internal byte[] ToByteArray()
+ {
+ byte[] data = new byte[this.LengthInBytes];
+ this.ToByteArray(data, 0);
+ return data;
+ }
+
+ internal void ToByteArray(byte[] buffer, int offset)
+ {
+ if (buffer == null)
+ throw new ArgumentNullException("buffer");
+
+ this.ZeroUnusedBits();
+ int end = this.Length / 32;
+ for (int i = 0; i < end; i++)
+ {
+ buffer[offset++] = (byte)(this.array[i] >> 24);
+ buffer[offset++] = (byte)(this.array[i] >> 16);
+ buffer[offset++] = (byte)(this.array[i] >> 8);
+ buffer[offset++] = (byte)(this.array[i] >> 0);
+ }
+
+ int shift = 24;
+ for (int i = end * 32; i < this.Length; i += 8)
+ {
+ buffer[offset++] = (byte)(this.array[this.array.Length - 1] >> shift);
+ shift -= 8;
+ }
+ }
+
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder(this.array.Length * 16);
+ for (int i = 0; i < this.Length; i++)
+ {
+ sb.Append(this.Get(i) ? 'T' : 'F');
+ sb.Append(' ');
+ }
+
+ return sb.ToString(0, sb.Length - 1);
+ }
+
+ public int TrueCount
+ {
+ get { return this.trueCount; }
+ }
+
+ void Validate()
+ {
+ this.ZeroUnusedBits();
+
+ // Update the population count
+ uint count = 0;
+ for (int i = 0; i < this.array.Length; i++)
+ {
+ uint v = (uint)this.array[i];
+ v = v - ((v >> 1) & 0x55555555);
+ v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
+ count += (((v + (v >> 4) & 0xF0F0F0F) * 0x1010101)) >> 24;
+ }
+ this.trueCount = (int)count ;
+ }
+
+ void ZeroUnusedBits()
+ {
+ if (this.array.Length == 0)
+ return;
+
+ // Zero the unused bits
+ int shift = 32 - this.length % 32;
+ if (shift != 0)
+ this.array[this.array.Length - 1] &= (-1 << shift);
+ }
+
+ void Check(BitField value)
+ {
+ MonoTorrent.Check.Value(value);
+ if (this.length != value.length)
+ throw new ArgumentException("BitFields are of different lengths", "value");
+ }
+
+ #endregion
+ }
+}
diff --git a/src/MonoTorrent/Check.cs b/src/MonoTorrent/Check.cs
new file mode 100644
index 000000000..1d2cde648
--- /dev/null
+++ b/src/MonoTorrent/Check.cs
@@ -0,0 +1,235 @@
+using System;
+
+namespace MonoTorrent
+{
+ public static class Check
+ {
+ static void DoCheck(object toCheck, string name)
+ {
+ if (toCheck == null)
+ throw new ArgumentNullException(name);
+ }
+
+ static void IsNullOrEmpty(string toCheck, string name)
+ {
+ DoCheck(toCheck, name);
+ if (toCheck.Length == 0)
+ throw new ArgumentException("Cannot be empty", name);
+ }
+
+ public static void Address(object address)
+ {
+ DoCheck(address, "address");
+ }
+
+ public static void AddressRange(object addressRange)
+ {
+ DoCheck(addressRange, "addressRange");
+ }
+
+ public static void AddressRanges(object addressRanges)
+ {
+ DoCheck(addressRanges, "addressRanges");
+ }
+
+ public static void Announces(object announces)
+ {
+ DoCheck(announces, "announces");
+ }
+
+ public static void BaseDirectory(object baseDirectory)
+ {
+ DoCheck(baseDirectory, "baseDirectory");
+ }
+
+ internal static void BaseType(Type baseType)
+ {
+ DoCheck(baseType, "baseType");
+ }
+
+ internal static void Buffer(object buffer)
+ {
+ DoCheck(buffer, "buffer");
+ }
+
+ internal static void Cache(object cache)
+ {
+ DoCheck(cache, "cache");
+ }
+
+ public static void Data(object data)
+ {
+ DoCheck(data, "data");
+ }
+
+ public static void Destination (object destination)
+ {
+ DoCheck (destination, "destination");
+ }
+
+ public static void Endpoint(object endpoint)
+ {
+ DoCheck(endpoint, "endpoint");
+ }
+
+ public static void File(object file)
+ {
+ DoCheck(file, "file");
+ }
+
+ public static void Files(object files)
+ {
+ DoCheck(files, "files");
+ }
+
+ public static void FileSource(object fileSource)
+ {
+ DoCheck(fileSource, "fileSource");
+ }
+
+ public static void InfoHash(object infoHash)
+ {
+ DoCheck(infoHash, "infoHash");
+ }
+
+ public static void Key (object key)
+ {
+ DoCheck (key, "key");
+ }
+
+ public static void Limiter(object limiter)
+ {
+ DoCheck(limiter, "limiter");
+ }
+
+ public static void Listener(object listener)
+ {
+ DoCheck(listener, "listener");
+ }
+
+ public static void Location(object location)
+ {
+ DoCheck(location, "location");
+ }
+
+ public static void MagnetLink(object magnetLink)
+ {
+ DoCheck(magnetLink, "magnetLink");
+ }
+
+ public static void Manager(object manager)
+ {
+ DoCheck(manager, "manager");
+ }
+
+ public static void Mappings (object mappings)
+ {
+ DoCheck (mappings, "mappings");
+ }
+
+ public static void Metadata(object metadata)
+ {
+ DoCheck(metadata, "metadata");
+ }
+
+ public static void Name (object name)
+ {
+ DoCheck (name, "name");
+ }
+
+ public static void Path(object path)
+ {
+ DoCheck(path, "path");
+ }
+
+ public static void Paths (object paths)
+ {
+ DoCheck (paths, "paths");
+ }
+
+ public static void PathNotEmpty(string path)
+ {
+ IsNullOrEmpty(path, "path");
+ }
+
+ public static void Peer (object peer)
+ {
+ DoCheck (peer, "peer");
+ }
+
+ public static void Peers (object peers)
+ {
+ DoCheck (peers, "peers");
+ }
+
+ public static void Picker(object picker)
+ {
+ DoCheck(picker, "picker");
+ }
+
+ public static void Result(object result)
+ {
+ DoCheck(result, "result");
+ }
+
+ public static void SavePath(object savePath)
+ {
+ DoCheck(savePath, "savePath");
+ }
+
+ public static void Settings(object settings)
+ {
+ DoCheck(settings, "settings");
+ }
+
+ internal static void SpecificType(Type specificType)
+ {
+ DoCheck(specificType, "specificType");
+ }
+
+ public static void Stream(object stream)
+ {
+ DoCheck(stream, "stream");
+ }
+
+ public static void Torrent(object torrent)
+ {
+ DoCheck(torrent, "torrent");
+ }
+
+ public static void TorrentInformation(object torrentInformation)
+ {
+ DoCheck(torrentInformation, "torrentInformation");
+ }
+
+ public static void TorrentSave(object torrentSave)
+ {
+ DoCheck(torrentSave, "torrentSave");
+ }
+
+ public static void Tracker(object tracker)
+ {
+ DoCheck(tracker, "tracker");
+ }
+
+ public static void Url(object url)
+ {
+ DoCheck(url, "url");
+ }
+
+ public static void Uri(Uri uri)
+ {
+ DoCheck(uri, "uri");
+ }
+
+ public static void Value(object value)
+ {
+ DoCheck(value, "value");
+ }
+
+ public static void Writer(object writer)
+ {
+ DoCheck(writer, "writer");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MonoTorrent/Enums.cs b/src/MonoTorrent/Enums.cs
new file mode 100644
index 000000000..56779fc27
--- /dev/null
+++ b/src/MonoTorrent/Enums.cs
@@ -0,0 +1,13 @@
+namespace MonoTorrent
+{
+ public enum Priority
+ {
+ DoNotDownload = 0,
+ Lowest = 1,
+ Low = 2,
+ Normal = 4,
+ High = 8,
+ Highest = 16,
+ Immediate = 32
+ }
+}
diff --git a/src/MonoTorrent/Exceptions/MessageException.cs b/src/MonoTorrent/Exceptions/MessageException.cs
new file mode 100644
index 000000000..87d3c26ab
--- /dev/null
+++ b/src/MonoTorrent/Exceptions/MessageException.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace MonoTorrent.Exceptions
+{
+ public class MessageException : TorrentException
+ {
+ public MessageException()
+ : base()
+ {
+ }
+
+
+ public MessageException(string message)
+ : base(message)
+ {
+ }
+
+
+ public MessageException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+
+ public MessageException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/src/MonoTorrent/HashAlgoFactory.cs b/src/MonoTorrent/HashAlgoFactory.cs
new file mode 100644
index 000000000..13b119729
--- /dev/null
+++ b/src/MonoTorrent/HashAlgoFactory.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+
+namespace MonoTorrent
+{
+ public static class HashAlgoFactory
+ {
+ static Dictionary algos = new Dictionary();
+
+ static HashAlgoFactory()
+ {
+ Register();
+ Register();
+ }
+
+ public static void Register()
+ where T : HashAlgorithm
+ where U : HashAlgorithm
+ {
+ Register(typeof(T), typeof(U));
+ }
+
+ public static void Register(Type baseType, Type specificType)
+ {
+ Check.BaseType(baseType);
+ Check.SpecificType(specificType);
+
+ lock (algos)
+ algos[baseType] = specificType;
+ }
+
+ public static T Create()
+ where T : HashAlgorithm
+ {
+ if (algos.ContainsKey(typeof(T)))
+ return (T)Activator.CreateInstance(algos[typeof(T)]);
+ return null;
+ }
+ }
+}
diff --git a/src/MonoTorrent/Hashes.cs b/src/MonoTorrent/Hashes.cs
new file mode 100644
index 000000000..f674f9e63
--- /dev/null
+++ b/src/MonoTorrent/Hashes.cs
@@ -0,0 +1,93 @@
+using System;
+
+namespace MonoTorrent
+{
+ public class Hashes
+ {
+ #region Constants
+ ///
+ /// Hash code length (in bytes)
+ ///
+ internal static readonly int HashCodeLength = 20;
+ #endregion
+
+
+ #region Private Fields
+
+ private int count;
+ private byte[] hashData;
+
+ #endregion Private Fields
+
+
+ #region Properties
+
+ ///
+ /// Number of Hashes (equivalent to number of Pieces)
+ ///
+ public int Count
+ {
+ get { return this.count; }
+ }
+
+ #endregion Properties
+
+
+ #region Constructors
+
+ internal Hashes(byte[] hashData, int count)
+ {
+ this.hashData = hashData;
+ this.count = count;
+ }
+
+ #endregion Constructors
+
+
+ #region Methods
+
+ ///
+ /// Determine whether a calculated hash is equal to our stored hash
+ ///
+ /// Hash code to check
+ /// Index of hash/piece to verify against
+ /// true iff hash is equal to our stored hash, false otherwise
+ public bool IsValid(byte[] hash, int hashIndex)
+ {
+ if (hash == null)
+ throw new ArgumentNullException("hash");
+
+ if (hash.Length != HashCodeLength)
+ throw new ArgumentException(string.Format("Hash must be {0} bytes in length", HashCodeLength), "hash");
+
+ if (hashIndex < 0 || hashIndex > this.count)
+ throw new ArgumentOutOfRangeException("hashIndex", string.Format("hashIndex must be between 0 and {0}", this.count));
+
+ int start = hashIndex * HashCodeLength;
+ for (int i = 0; i < HashCodeLength; i++)
+ if (hash[i] != this.hashData[i + start])
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Returns the hash for a specific piece
+ ///
+ /// Piece/hash index to return
+ /// byte[] (length HashCodeLength) containing hashdata
+ public byte[] ReadHash(int hashIndex)
+ {
+ if (hashIndex < 0 || hashIndex >= this.count)
+ throw new ArgumentOutOfRangeException("hashIndex");
+
+ // Read out our specified piece's hash data
+ byte[] hash = new byte[HashCodeLength];
+ Buffer.BlockCopy(this.hashData, hashIndex * HashCodeLength, hash, 0, HashCodeLength);
+
+ return hash;
+ }
+
+ #endregion Methods
+ }
+}
diff --git a/src/MonoTorrent/InfoHash.cs b/src/MonoTorrent/InfoHash.cs
new file mode 100644
index 000000000..dc9fc44a8
--- /dev/null
+++ b/src/MonoTorrent/InfoHash.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MonoTorrent.Common;
+using System.Web;
+
+namespace MonoTorrent
+{
+ public class InfoHash : IEquatable
+ {
+ static Dictionary base32DecodeTable;
+
+ static InfoHash()
+ {
+ base32DecodeTable = new Dictionary();
+ string table = "abcdefghijklmnopqrstuvwxyz234567";
+ for (int i = 0; i < table.Length; i++)
+ base32DecodeTable[table[i]] = (byte)i;
+ }
+
+ byte[] hash;
+
+ internal byte[] Hash
+ {
+ get { return hash; }
+ }
+
+ public InfoHash(byte[] infoHash)
+ {
+ Check.InfoHash(infoHash);
+ if (infoHash.Length != 20)
+ throw new ArgumentException("Infohash must be exactly 20 bytes long");
+ hash = (byte[])infoHash.Clone();
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as InfoHash);
+ }
+
+ public bool Equals(byte[] other)
+ {
+ return other == null || other.Length != 20 ? false : Toolbox.ByteMatch(Hash, other);
+ }
+
+ public bool Equals(InfoHash other)
+ {
+ return this == other;
+ }
+
+ public override int GetHashCode()
+ {
+ // Equality is based generally on checking 20 positions, checking 4 should be enough
+ // for the hashcode as infohashes are randomly distributed.
+ return Hash[0] | (Hash[1] << 8) | (Hash[2] << 16) | (Hash[3] << 24);
+ }
+
+ public byte[] ToArray()
+ {
+ return (byte[])hash.Clone();
+ }
+
+ public string ToHex()
+ {
+ StringBuilder sb = new StringBuilder(40);
+ for (int i = 0; i < hash.Length; i++)
+ {
+ string hex = hash[i].ToString("X");
+ if (hex.Length != 2)
+ sb.Append("0");
+ sb.Append(hex);
+ }
+ return sb.ToString();
+ }
+
+ public override string ToString()
+ {
+ return BitConverter.ToString(hash);
+ }
+
+ public string UrlEncode()
+ {
+ return UriHelper.UrlEncode(Hash);
+ }
+
+ public static bool operator ==(InfoHash left, InfoHash right)
+ {
+ if ((object)left == null)
+ return (object)right == null;
+ if ((object)right == null)
+ return false;
+ return Toolbox.ByteMatch(left.Hash, right.Hash);
+ }
+
+ public static bool operator !=(InfoHash left, InfoHash right)
+ {
+ return !(left == right);
+ }
+
+ public static InfoHash FromBase32(string infoHash)
+ {
+ Check.InfoHash (infoHash);
+ if (infoHash.Length != 32)
+ throw new ArgumentException("Infohash must be a base32 encoded 32 character string");
+
+ infoHash = infoHash.ToLower();
+ int infohashOffset =0 ;
+ byte[] hash = new byte[20];
+ var temp = new byte[8];
+ for (int i = 0; i < hash.Length; ) {
+ for (int j=0; j < 8; j++)
+ if (!base32DecodeTable.TryGetValue(infoHash[infohashOffset++], out temp[j]))
+ throw new ArgumentException ("infoHash", "Value is not a valid base32 encoded string");
+
+ //8 * 5bits = 40 bits = 5 bytes
+ hash[i++] = (byte)((temp[0] << 3) | (temp [1]>> 2));
+ hash[i++] = (byte)((temp[1] << 6) | (temp[2] << 1) | (temp[3] >> 4));
+ hash[i++] = (byte)((temp[3] << 4) | (temp [4]>> 1));
+ hash[i++] = (byte)((temp[4] << 7) | (temp[5] << 2) | (temp [6]>> 3));
+ hash[i++] = (byte)((temp[6] << 5) | temp[7]);
+ }
+
+ return new InfoHash(hash);
+ }
+
+ public static InfoHash FromHex(string infoHash)
+ {
+ Check.InfoHash (infoHash);
+ if (infoHash.Length != 40)
+ throw new ArgumentException("Infohash must be 40 characters long");
+
+ byte[] hash = new byte[20];
+ for (int i = 0; i < hash.Length; i++)
+ hash[i] = byte.Parse(infoHash.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber);
+
+ return new InfoHash(hash);
+ }
+
+ public static InfoHash FromMagnetLink(string magnetLink)
+ {
+ Check.MagnetLink(magnetLink);
+ if (!magnetLink.StartsWith("magnet:?"))
+ throw new ArgumentException("Invalid magnet link format");
+ magnetLink = magnetLink.Substring("magnet:?".Length);
+ int hashStart = magnetLink.IndexOf("xt=urn:btih:");
+ if (hashStart == -1)
+ throw new ArgumentException("Magnet link does not contain an infohash");
+ hashStart += "xt=urn:btih:".Length;
+
+ int hashEnd = magnetLink.IndexOf('&', hashStart);
+ if (hashEnd == -1)
+ hashEnd = magnetLink.Length;
+
+ switch (hashEnd - hashStart)
+ {
+ case 32:
+ return FromBase32(magnetLink.Substring(hashStart, 32));
+ case 40:
+ return FromHex(magnetLink.Substring(hashStart, 40));
+ default:
+ throw new ArgumentException("Infohash must be base32 or hex encoded.");
+ }
+ }
+
+ public static InfoHash UrlDecode(string infoHash)
+ {
+ Check.InfoHash(infoHash);
+ return new InfoHash(UriHelper.UrlDecode(infoHash));
+ }
+ }
+}
diff --git a/src/MonoTorrent/MagnetLink.cs b/src/MonoTorrent/MagnetLink.cs
new file mode 100644
index 000000000..1ee56c4c8
--- /dev/null
+++ b/src/MonoTorrent/MagnetLink.cs
@@ -0,0 +1,95 @@
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoTorrent
+{
+ public class MagnetLink
+ {
+ public RawTrackerTier AnnounceUrls {
+ get; private set;
+ }
+
+ public InfoHash InfoHash {
+ get; private set;
+ }
+
+ public string Name {
+ get; private set;
+ }
+
+ public List Webseeds {
+ get; private set;
+ }
+
+ public MagnetLink (string url)
+ {
+ Check.Url (url);
+ AnnounceUrls = new RawTrackerTier ();
+ Webseeds = new List ();
+
+ ParseMagnetLink (url);
+ }
+
+ void ParseMagnetLink (string url)
+ {
+ string[] splitStr = url.Split ('?');
+ if (splitStr.Length == 0 || splitStr[0] != "magnet:")
+ throw new FormatException ("The magnet link must start with 'magnet:?'.");
+
+ if (splitStr.Length == 1)
+ return;//no parametter
+
+ string[] parameters = splitStr[1].Split ('&', ';');
+
+ for (int i = 0; i < parameters.Length ; i++)
+ {
+ string[] keyval = parameters[i].Split ('=');
+ if (keyval.Length != 2)
+ throw new FormatException ("A field-value pair of the magnet link contain more than one equal'.");
+ switch (keyval[0].Substring(0, 2))
+ {
+ case "xt"://exact topic
+ if (InfoHash != null)
+ throw new FormatException ("More than one infohash in magnet link is not allowed.");
+
+ string val = keyval[1].Substring(9);
+ switch (keyval[1].Substring(0, 9))
+ {
+ case "urn:sha1:"://base32 hash
+ case "urn:btih:":
+ if (val.Length == 32)
+ InfoHash = InfoHash.FromBase32 (val);
+ else if (val.Length == 40)
+ InfoHash = InfoHash.FromHex (val);
+ else
+ throw new FormatException("Infohash must be base32 or hex encoded.");
+ break;
+ }
+ break;
+ case "tr" ://address tracker
+ var bytes = UriHelper.UrlDecode(keyval[1]);
+ AnnounceUrls.Add(Encoding.UTF8.GetString(bytes));
+ break;
+ case "as"://Acceptable Source
+ Webseeds.Add (keyval[1]);
+ break;
+ case "dn"://display name
+ var name = UriHelper.UrlDecode(keyval[1]);
+ Name = Encoding.UTF8.GetString(name);
+ break;
+ case "xl"://exact length
+ case "xs":// eXact Source - P2P link.
+ case "kt"://keyword topic
+ case "mt"://manifest topic
+ //not supported for moment
+ break;
+ default:
+ //not supported
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/MonoTorrent/Messages/IMessage.cs b/src/MonoTorrent/Messages/IMessage.cs
new file mode 100644
index 000000000..62512b6c7
--- /dev/null
+++ b/src/MonoTorrent/Messages/IMessage.cs
@@ -0,0 +1,12 @@
+namespace MonoTorrent.Messages
+{
+ interface IMessage
+ {
+ int ByteLength { get;}
+
+ byte[] Encode();
+ int Encode(byte[] buffer, int offset);
+
+ void Decode(byte[] buffer, int offset, int length);
+ }
+}
diff --git a/src/MonoTorrent/Messages/Message.cs b/src/MonoTorrent/Messages/Message.cs
new file mode 100644
index 000000000..bd59d9952
--- /dev/null
+++ b/src/MonoTorrent/Messages/Message.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Net;
+using MonoTorrent.Exceptions;
+
+namespace MonoTorrent.Messages
+{
+ public abstract class Message : IMessage
+ {
+ public abstract int ByteLength { get; }
+
+ protected int CheckWritten(int written)
+ {
+ if (written != this.ByteLength)
+ throw new MessageException("Message encoded incorrectly. Incorrect number of bytes written");
+ return written;
+ }
+
+ public abstract void Decode(byte[] buffer, int offset, int length);
+
+ public byte[] Encode()
+ {
+ byte[] buffer = new byte[this.ByteLength];
+ this.Encode(buffer, 0);
+ return buffer;
+ }
+
+ public abstract int Encode(byte[] buffer, int offset);
+
+ static public byte ReadByte(byte[] buffer, int offset)
+ {
+ return buffer[offset];
+ }
+
+ static public byte ReadByte(byte[] buffer, ref int offset)
+ {
+ byte b = buffer[offset];
+ offset++;
+ return b;
+ }
+
+ static public byte[] ReadBytes(byte[] buffer, int offset, int count)
+ {
+ return ReadBytes(buffer, ref offset, count);
+ }
+
+ static public byte[] ReadBytes(byte[] buffer, ref int offset, int count)
+ {
+ byte[] result = new byte[count];
+ Buffer.BlockCopy(buffer, offset, result, 0, count);
+ offset += count;
+ return result;
+ }
+
+ static public short ReadShort(byte[] buffer, int offset)
+ {
+ return ReadShort(buffer, ref offset);
+ }
+
+ static public short ReadShort(byte[] buffer, ref int offset)
+ {
+ short ret = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(buffer, offset));
+ offset += 2;
+ return ret;
+ }
+
+ static public string ReadString(byte[] buffer, int offset, int count)
+ {
+ return ReadString(buffer, ref offset, count);
+ }
+
+ static public string ReadString(byte[] buffer, ref int offset, int count)
+ {
+ string s = System.Text.Encoding.ASCII.GetString(buffer, offset, count);
+ offset += count;
+ return s;
+ }
+
+ static public int ReadInt(byte[] buffer, int offset)
+ {
+ return ReadInt(buffer, ref offset);
+ }
+
+ static public int ReadInt(byte[] buffer, ref int offset)
+ {
+ int ret = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buffer, offset));
+ offset += 4;
+ return ret;
+ }
+
+ static public long ReadLong(byte[] buffer, int offset)
+ {
+ return ReadLong(buffer, ref offset);
+ }
+
+ static public long ReadLong(byte[] buffer, ref int offset)
+ {
+ long ret = IPAddress.NetworkToHostOrder(BitConverter.ToInt64(buffer, offset));
+ offset += 8;
+ return ret;
+ }
+
+ static public int Write(byte[] buffer, int offset, byte value)
+ {
+ buffer[offset] = value;
+ return 1;
+ }
+
+ static public int Write(byte[] dest, int destOffset, byte[] src, int srcOffset, int count)
+ {
+ Buffer.BlockCopy(src, srcOffset, dest, destOffset, count);
+ return count;
+ }
+
+ static public int Write(byte[] buffer, int offset, ushort value)
+ {
+ return Write(buffer, offset, (short)value);
+ }
+
+ static public int Write(byte[] buffer, int offset, short value)
+ {
+ offset += Write(buffer, offset, (byte)(value >> 8));
+ offset += Write(buffer, offset, (byte)value);
+ return 2;
+ }
+
+ static public int Write(byte[] buffer, int offset, int value)
+ {
+ offset += Write(buffer, offset, (byte)(value >> 24));
+ offset += Write(buffer, offset, (byte)(value >> 16));
+ offset += Write(buffer, offset, (byte)(value >> 8));
+ offset += Write(buffer, offset, (byte)(value));
+ return 4;
+ }
+
+ static public int Write(byte[] buffer, int offset, uint value)
+ {
+ return Write(buffer, offset, (int)value);
+ }
+
+ static public int Write(byte[] buffer, int offset, long value)
+ {
+ offset += Write(buffer, offset, (int)(value >> 32));
+ offset += Write(buffer, offset, (int)value);
+ return 8;
+ }
+
+ static public int Write(byte[] buffer, int offset, ulong value)
+ {
+ return Write(buffer, offset, (long)value);
+ }
+
+ static public int Write(byte[] buffer, int offset, byte[] value)
+ {
+ return Write(buffer, offset, value, 0, value.Length);
+ }
+
+ static public int WriteAscii(byte[] buffer, int offset, string text)
+ {
+ for (int i = 0; i < text.Length; i++)
+ Write(buffer, offset + i, (byte)text[i]);
+ return text.Length;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MonoTorrent/MonoTorrent.csproj b/src/MonoTorrent/MonoTorrent.csproj
new file mode 100644
index 000000000..dd8fd6907
--- /dev/null
+++ b/src/MonoTorrent/MonoTorrent.csproj
@@ -0,0 +1,117 @@
+
+
+
+ Debug
+ x86
+ Local
+ 9.0.21022
+ 2.0
+ {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}
+ Library
+ MonoTorrent
+ MonoTorrent
+
+
+ JScript
+ Grid
+ IE50
+ false
+
+
+ MonoTorrent
+
+
+ 3.5
+
+
+ v4.0
+
+ 512
+ publish\
+ true
+ Disk
+ false
+ Foreground
+ 7
+ Days
+ false
+ false
+ true
+ 0
+ 1.0.0.%2a
+ false
+ false
+ true
+ ..\
+
+
+ x86
+ true
+ full
+ false
+ ..\..\_output\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ x86
+ pdbonly
+ true
+ ..\..\_output\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+ .NET Framework 2.0 %28x86%29
+ true
+
+
+ False
+ .NET Framework 3.0 %28x86%29
+ false
+
+
+ False
+ .NET Framework 3.5
+ false
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/MonoTorrent/RawTrackerTier.cs b/src/MonoTorrent/RawTrackerTier.cs
new file mode 100644
index 000000000..43677fa4e
--- /dev/null
+++ b/src/MonoTorrent/RawTrackerTier.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using MonoTorrent.BEncoding;
+
+namespace MonoTorrent
+{
+ public class RawTrackerTier : IList
+ {
+ public string this[int index] {
+ get { return ((BEncodedString) Tier [index]).Text; }
+ set { Tier [index] = new BEncodedString (value );}
+ }
+
+ internal BEncodedList Tier {
+ get; set;
+ }
+
+ public RawTrackerTier ()
+ : this (new BEncodedList ())
+ {
+ }
+
+ public RawTrackerTier (BEncodedList tier)
+ {
+ Tier = tier;
+ }
+
+ public RawTrackerTier (IEnumerable announces)
+ : this ()
+ {
+ foreach (var v in announces)
+ Add (v);
+ }
+
+ public int IndexOf (string item)
+ {
+ return Tier.IndexOf ((BEncodedString) item);
+ }
+
+ public void Insert (int index, string item)
+ {
+ Tier.Insert (index, (BEncodedString) item);
+ }
+
+ public void RemoveAt (int index)
+ {
+ Tier.RemoveAt (index);
+ }
+
+ public void Add (string item)
+ {
+ Tier.Add ((BEncodedString) item);
+ }
+
+ public void Clear ()
+ {
+ Tier.Clear ();
+ }
+
+ public bool Contains (string item)
+ {
+ return Tier.Contains ((BEncodedString) item);
+ }
+
+ public void CopyTo (string[] array, int arrayIndex)
+ {
+ foreach (var s in this)
+ array [arrayIndex ++] = s;
+ }
+
+ public bool Remove (string item)
+ {
+ return Tier.Remove ((BEncodedString) item);
+ }
+
+ public int Count {
+ get { return Tier.Count; }
+ }
+
+ public bool IsReadOnly {
+ get { return Tier.IsReadOnly; }
+ }
+
+ public IEnumerator GetEnumerator ()
+ {
+ foreach (BEncodedString v in Tier)
+ yield return v.Text;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator ()
+ {
+ return GetEnumerator ();
+ }
+ }
+}
diff --git a/src/MonoTorrent/RawTrackerTiers.cs b/src/MonoTorrent/RawTrackerTiers.cs
new file mode 100644
index 000000000..5ea50e3ab
--- /dev/null
+++ b/src/MonoTorrent/RawTrackerTiers.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using MonoTorrent.BEncoding;
+
+namespace MonoTorrent
+{
+ public class RawTrackerTiers : IList
+ {
+ BEncodedList Tiers {
+ get; set;
+ }
+
+ public RawTrackerTiers ()
+ : this (new BEncodedList ())
+ {
+ }
+
+ public RawTrackerTiers (BEncodedList tiers)
+ {
+ Tiers = tiers;
+ }
+
+ public int IndexOf (RawTrackerTier item)
+ {
+ if (item != null) {
+ for (int i = 0; i < Tiers.Count; i++)
+ if (item.Tier == Tiers [i])
+ return i;
+ }
+ return -1;
+ }
+
+ public void Insert (int index, RawTrackerTier item)
+ {
+ Tiers.Insert (index, item.Tier);
+ }
+
+ public void RemoveAt (int index)
+ {
+ Tiers.RemoveAt (index);
+ }
+
+ public RawTrackerTier this[int index] {
+ get { return new RawTrackerTier ((BEncodedList) Tiers [index]); }
+ set { Tiers [index] = value.Tier; }
+ }
+
+ public void Add (RawTrackerTier item)
+ {
+ Tiers.Add (item.Tier);
+ }
+
+ public void AddRange (IEnumerable tiers)
+ {
+ foreach (var v in tiers)
+ Add (v);
+ }
+
+ public void Clear ()
+ {
+ Tiers.Clear ();
+ }
+
+ public bool Contains (RawTrackerTier item)
+ {
+ return IndexOf (item) != -1;
+ }
+
+ public void CopyTo (RawTrackerTier[] array, int arrayIndex)
+ {
+ foreach (var v in this)
+ array [arrayIndex ++] = v;
+ }
+
+ public bool Remove (RawTrackerTier item)
+ {
+ int index = IndexOf (item);
+ if (index != -1)
+ RemoveAt (index);
+
+ return index != -1;
+ }
+
+ public int Count {
+ get { return Tiers.Count; }
+ }
+
+ public bool IsReadOnly {
+ get { return Tiers.IsReadOnly; }
+ }
+
+ public IEnumerator GetEnumerator ()
+ {
+ foreach (var v in Tiers)
+ yield return new RawTrackerTier ((BEncodedList) v);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator ()
+ {
+ return GetEnumerator ();
+ }
+ }
+}
diff --git a/src/MonoTorrent/ToolBox.cs b/src/MonoTorrent/ToolBox.cs
new file mode 100644
index 000000000..b4116d716
--- /dev/null
+++ b/src/MonoTorrent/ToolBox.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections;
+using System.Text;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace MonoTorrent.Common
+{
+ public delegate long Operation(T target);
+
+ public static class Toolbox
+ {
+ private static Random r = new Random();
+ public static int Count(IEnumerable enumerable, Predicate predicate)
+ {
+ int count = 0;
+
+ foreach (T t in enumerable)
+ if (predicate(t))
+ count++;
+
+ return count;
+ }
+
+ public static long Accumulate(IEnumerable enumerable, Operation action)
+ {
+ long count = 0;
+
+ foreach (T t in enumerable)
+ count += action(t);
+
+ return count;
+ }
+
+ public static void RaiseAsyncEvent(EventHandler e, object o, T args)
+ where T : EventArgs
+ {
+ if (e == null)
+ return;
+
+ ThreadPool.QueueUserWorkItem(delegate {
+ if (e != null)
+ e(o, args);
+ });
+ }
+ ///
+ /// Randomizes the contents of the array
+ ///
+ ///
+ ///
+ public static void Randomize(List array)
+ {
+ List clone = new List(array);
+ array.Clear();
+
+ while (clone.Count > 0)
+ {
+ int index = r.Next(0, clone.Count);
+ array.Add(clone[index]);
+ clone.RemoveAt(index);
+ }
+ }
+
+ ///
+ /// Switches the positions of two elements in an array
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Switch(IList array, int first, int second)
+ {
+ T obj = array[first];
+ array[first] = array[second];
+ array[second] = obj;
+ }
+
+ ///
+ /// Checks to see if the contents of two byte arrays are equal
+ ///
+ /// The first array
+ /// The second array
+ /// True if the arrays are equal, false if they aren't
+ public static bool ByteMatch(byte[] array1, byte[] array2)
+ {
+ if (array1 == null)
+ throw new ArgumentNullException("array1");
+ if (array2 == null)
+ throw new ArgumentNullException("array2");
+
+ if (array1.Length != array2.Length)
+ return false;
+
+ return ByteMatch(array1, 0, array2, 0, array1.Length);
+ }
+
+ ///
+ /// Checks to see if the contents of two byte arrays are equal
+ ///
+ /// The first array
+ /// The second array
+ /// The starting index for the first array
+ /// The starting index for the second array
+ /// The number of bytes to check
+ ///
+ public static bool ByteMatch(byte[] array1, int offset1, byte[] array2, int offset2, int count)
+ {
+ if (array1 == null)
+ throw new ArgumentNullException("array1");
+ if (array2 == null)
+ throw new ArgumentNullException("array2");
+
+ // If either of the arrays is too small, they're not equal
+ if ((array1.Length - offset1) < count || (array2.Length - offset2) < count)
+ return false;
+
+ // Check if any elements are unequal
+ for (int i = 0; i < count; i++)
+ if (array1[offset1 + i] != array2[offset2 + i])
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/src/MonoTorrent/Torrent.cs b/src/MonoTorrent/Torrent.cs
new file mode 100644
index 000000000..59a1f3253
--- /dev/null
+++ b/src/MonoTorrent/Torrent.cs
@@ -0,0 +1,885 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+using MonoTorrent.BEncoding;
+using MonoTorrent.Common;
+
+namespace MonoTorrent
+{
+ ///
+ /// The "Torrent" class for both Tracker and Client should inherit from this
+ /// as it contains the fields that are common to both.
+ ///
+ public class Torrent : IEquatable
+ {
+ #region Private Fields
+
+ private BEncodedDictionary originalDictionary;
+ private BEncodedValue azureusProperties;
+ private IList announceUrls;
+ private string comment;
+ private string createdBy;
+ private DateTime creationDate;
+ private byte[] ed2k;
+ private string encoding;
+ internal InfoHash infoHash;
+ private bool isPrivate;
+ protected string name;
+ private BEncodedList nodes;
+ protected int pieceLength;
+ protected Hashes pieces;
+ private string publisher;
+ private string publisherUrl;
+ private byte[] sha1;
+ protected long size;
+ private string source;
+ protected TorrentFile[] torrentFiles;
+ protected string torrentPath;
+ private List getRightHttpSeeds;
+ private byte[] metadata;
+
+ #endregion Private Fields
+
+
+ #region Properties
+
+ internal byte[] Metadata
+ {
+ get { return this.metadata; }
+ }
+
+ ///
+ /// The announce URLs contained within the .torrent file
+ ///
+ public IList AnnounceUrls
+ {
+ get { return this.announceUrls; }
+ }
+
+
+ ///
+ /// This dictionary is specific for azureus client
+ /// It can contain
+ /// dht_backup_enable (number)
+ /// Content (dictionnary)
+ /// Publisher
+ /// Description
+ /// Title
+ /// Creation Date
+ /// Content Hash
+ /// Revision Date
+ /// Thumbnail (string) = Base64 encoded image
+ /// Progressive
+ /// Speed Bps (number)
+ /// but not useful for MT
+ ///
+ public BEncodedValue AzureusProperties
+ {
+ get { return this.azureusProperties; }
+ }
+
+
+ ///
+ /// The comment contained within the .torrent file
+ ///
+ public string Comment
+ {
+ get { return this.comment; }
+ }
+
+
+ ///
+ /// The optional string showing who/what created the .torrent
+ ///
+ public string CreatedBy
+ {
+ get { return this.createdBy; }
+ }
+
+
+ ///
+ /// The creation date of the .torrent file
+ ///
+ public DateTime CreationDate
+ {
+ get { return this.creationDate; }
+ }
+
+
+ ///
+ /// The optional ED2K hash contained within the .torrent file
+ ///
+ public byte[] ED2K
+ {
+ get { return this.ed2k; }
+ }
+
+
+ ///
+ /// The encoding used by the client that created the .torrent file
+ ///
+ public string Encoding
+ {
+ get { return this.encoding; }
+ }
+
+
+ ///
+ /// The list of files contained within the .torrent which are available for download
+ ///
+ public TorrentFile[] Files
+ {
+ get { return this.torrentFiles; }
+ }
+
+
+ ///
+ /// This is the infohash that is generated by putting the "Info" section of a .torrent
+ /// through a ManagedSHA1 hasher.
+ ///
+ public InfoHash InfoHash
+ {
+ get { return this.infoHash; }
+ }
+
+
+ ///
+ /// Shows whether DHT is allowed or not. If it is a private torrent, no peer
+ /// sharing should be allowed.
+ ///
+ public bool IsPrivate
+ {
+ get { return this.isPrivate; }
+ }
+
+
+ ///
+ /// In the case of a single file torrent, this is the name of the file.
+ /// In the case of a multi file torrent, it is the name of the root folder.
+ ///
+ public string Name
+ {
+ get { return this.name; }
+ private set { this.name = value; }
+ }
+
+
+ ///
+ /// FIXME: No idea what this is.
+ ///
+ public BEncodedList Nodes
+ {
+ get { return this.nodes; }
+ }
+
+
+ ///
+ /// The length of each piece in bytes.
+ ///
+ public int PieceLength
+ {
+ get { return this.pieceLength; }
+ }
+
+
+ ///
+ /// This is the array of hashes contained within the torrent.
+ ///
+ public Hashes Pieces
+ {
+ get { return this.pieces; }
+ }
+
+
+ ///
+ /// The name of the Publisher
+ ///
+ public string Publisher
+ {
+ get { return this.publisher; }
+ }
+
+
+ ///
+ /// The Url of the publisher of either the content or the .torrent file
+ ///
+ public string PublisherUrl
+ {
+ get { return this.publisherUrl; }
+ }
+
+
+ ///
+ /// The optional SHA1 hash contained within the .torrent file
+ ///
+ public byte[] SHA1
+ {
+ get { return this.sha1; }
+ }
+
+
+ ///
+ /// The total size of all the files that have to be downloaded.
+ ///
+ public long Size
+ {
+ get { return this.size; }
+ private set { this.size = value; }
+ }
+
+
+ ///
+ /// The source of the .torrent file
+ ///
+ public string Source
+ {
+ get { return this.source; }
+ }
+
+
+ ///
+ /// This is the path at which the .torrent file is located
+ ///
+ public string TorrentPath
+ {
+ get { return this.torrentPath; }
+ internal set { this.torrentPath = value; }
+ }
+
+ ///
+ /// This is the http-based seeding (getright protocole)
+ ///
+ public List GetRightHttpSeeds
+ {
+ get { return this.getRightHttpSeeds; }
+ }
+
+ #endregion Properties
+
+
+ #region Constructors
+
+ protected Torrent()
+ {
+ this.announceUrls = new RawTrackerTiers ();
+ this.comment = string.Empty;
+ this.createdBy = string.Empty;
+ this.creationDate = new DateTime(1970, 1, 1, 0, 0, 0);
+ this.encoding = string.Empty;
+ this.name = string.Empty;
+ this.publisher = string.Empty;
+ this.publisherUrl = string.Empty;
+ this.source = string.Empty;
+ this.getRightHttpSeeds = new List();
+ }
+
+ #endregion
+
+
+ #region Public Methods
+
+ public override bool Equals(object obj)
+ {
+ return this.Equals(obj as Torrent);
+ }
+
+ public bool Equals(Torrent other)
+ {
+ if (other == null)
+ return false;
+
+ return this.infoHash == other.infoHash;
+ }
+
+ public override int GetHashCode()
+ {
+ return this.infoHash.GetHashCode();
+ }
+
+ internal byte [] ToBytes ()
+ {
+ return this.originalDictionary.Encode ();
+ }
+
+ internal BEncodedDictionary ToDictionary ()
+ {
+ // Give the user a copy of the original dictionary.
+ return BEncodedValue.Clone (this.originalDictionary);
+ }
+
+ public override string ToString()
+ {
+ return this.name;
+ }
+
+ #endregion Public Methods
+
+
+ #region Private Methods
+
+ ///
+ /// This method is called internally to read out the hashes from the info section of the
+ /// .torrent file.
+ ///
+ /// The byte[]containing the hashes from the .torrent file
+ private void LoadHashPieces(byte[] data)
+ {
+ if (data.Length % 20 != 0)
+ throw new TorrentException("Invalid infohash detected");
+
+ this.pieces = new Hashes(data, data.Length / 20);
+ }
+
+
+ ///
+ /// This method is called internally to load in all the files found within the "Files" section
+ /// of the .torrents infohash
+ ///
+ /// The list containing the files available to download
+ private void LoadTorrentFiles(BEncodedList list)
+ {
+ List files = new List();
+ int endIndex;
+ long length;
+ string path;
+ byte[] md5sum;
+ byte[] ed2k;
+ byte[] sha1;
+ int startIndex;
+ StringBuilder sb = new StringBuilder(32);
+
+ foreach (BEncodedDictionary dict in list)
+ {
+ length = 0;
+ path = null;
+ md5sum = null;
+ ed2k = null;
+ sha1 = null;
+
+ foreach (KeyValuePair keypair in dict)
+ {
+ switch (keypair.Key.Text)
+ {
+ case ("sha1"):
+ sha1 = ((BEncodedString)keypair.Value).TextBytes;
+ break;
+
+ case ("ed2k"):
+ ed2k = ((BEncodedString)keypair.Value).TextBytes;
+ break;
+
+ case ("length"):
+ length = long.Parse(keypair.Value.ToString());
+ break;
+
+ case ("path.utf-8"):
+ foreach (BEncodedString str in ((BEncodedList)keypair.Value))
+ {
+ sb.Append(str.Text);
+ sb.Append(Path.DirectorySeparatorChar);
+ }
+ path = sb.ToString(0, sb.Length - 1);
+ sb.Remove(0, sb.Length);
+ break;
+
+ case ("path"):
+ if (string.IsNullOrEmpty(path))
+ {
+ foreach (BEncodedString str in ((BEncodedList)keypair.Value))
+ {
+ sb.Append(str.Text);
+ sb.Append(Path.DirectorySeparatorChar);
+ }
+ path = sb.ToString(0, sb.Length - 1);
+ sb.Remove(0, sb.Length);
+ }
+ break;
+
+ case ("md5sum"):
+ md5sum = ((BEncodedString)keypair.Value).TextBytes;
+ break;
+
+ default:
+ break; //FIXME: Log unknown values
+ }
+ }
+
+ // A zero length file always belongs to the same piece as the previous file
+ if (length == 0)
+ {
+ if (files.Count > 0)
+ {
+ startIndex = files[files.Count - 1].EndPieceIndex;
+ endIndex = files[files.Count - 1].EndPieceIndex;
+ }
+ else
+ {
+ startIndex = 0;
+ endIndex = 0;
+ }
+ }
+ else
+ {
+ startIndex = (int)(this.size / this.pieceLength);
+ endIndex = (int)((this.size + length) / this.pieceLength);
+ if ((this.size + length) % this.pieceLength == 0)
+ endIndex--;
+ }
+ this.size += length;
+ files.Add(new TorrentFile(path, length, path, startIndex, endIndex, md5sum, ed2k, sha1));
+ }
+
+ this.torrentFiles = files.ToArray();
+ }
+
+
+ ///
+ /// This method is called internally to load the information found within the "Info" section
+ /// of the .torrent file
+ ///
+ /// The dictionary representing the Info section of the .torrent file
+ private void ProcessInfo(BEncodedDictionary dictionary)
+ {
+ this.metadata = dictionary.Encode();
+ this.pieceLength = int.Parse(dictionary["piece length"].ToString());
+ this.LoadHashPieces(((BEncodedString)dictionary["pieces"]).TextBytes);
+
+ foreach (KeyValuePair keypair in dictionary)
+ {
+ switch (keypair.Key.Text)
+ {
+ case ("source"):
+ this.source = keypair.Value.ToString();
+ break;
+
+ case ("sha1"):
+ this.sha1 = ((BEncodedString)keypair.Value).TextBytes;
+ break;
+
+ case ("ed2k"):
+ this.ed2k = ((BEncodedString)keypair.Value).TextBytes;
+ break;
+
+ case ("publisher-url.utf-8"):
+ if (keypair.Value.ToString().Length > 0)
+ this.publisherUrl = keypair.Value.ToString();
+ break;
+
+ case ("publisher-url"):
+ if ((String.IsNullOrEmpty(this.publisherUrl)) && (keypair.Value.ToString().Length > 0))
+ this.publisherUrl = keypair.Value.ToString();
+ break;
+
+ case ("publisher.utf-8"):
+ if (keypair.Value.ToString().Length > 0)
+ this.publisher = keypair.Value.ToString();
+ break;
+
+ case ("publisher"):
+ if ((String.IsNullOrEmpty(this.publisher)) && (keypair.Value.ToString().Length > 0))
+ this.publisher = keypair.Value.ToString();
+ break;
+
+ case ("files"):
+ this.LoadTorrentFiles(((BEncodedList)keypair.Value));
+ break;
+
+ case ("name.utf-8"):
+ if (keypair.Value.ToString().Length > 0)
+ this.name = keypair.Value.ToString();
+ break;
+
+ case ("name"):
+ if ((String.IsNullOrEmpty(this.name)) && (keypair.Value.ToString().Length > 0))
+ this.name = keypair.Value.ToString();
+ break;
+
+ case ("piece length"): // Already handled
+ break;
+
+ case ("length"):
+ break; // This is a singlefile torrent
+
+ case ("private"):
+ this.isPrivate = (keypair.Value.ToString() == "1") ? true : false;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ if (this.torrentFiles == null) // Not a multi-file torrent
+ {
+ long length = long.Parse(dictionary["length"].ToString());
+ this.size = length;
+ string path = this.name;
+ byte[] md5 = (dictionary.ContainsKey("md5")) ? ((BEncodedString)dictionary["md5"]).TextBytes : null;
+ byte[] ed2k = (dictionary.ContainsKey("ed2k")) ? ((BEncodedString)dictionary["ed2k"]).TextBytes : null;
+ byte[] sha1 = (dictionary.ContainsKey("sha1")) ? ((BEncodedString)dictionary["sha1"]).TextBytes : null;
+
+ this.torrentFiles = new TorrentFile[1];
+ int endPiece = Math.Min(this.Pieces.Count - 1, (int)((this.size + (this.pieceLength - 1)) / this.pieceLength));
+ this.torrentFiles[0] = new TorrentFile(path, length, path, 0, endPiece, md5, ed2k, sha1);
+ }
+ }
+
+ #endregion Private Methods
+
+
+ #region Loading methods
+
+ ///
+ /// This method loads a .torrent file from the specified path.
+ ///
+ /// The path to load the .torrent file from
+ public static Torrent Load(string path)
+ {
+ Check.Path(path);
+
+ using (Stream s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ return Torrent.Load(s, path);
+ }
+
+ ///
+ /// Loads a torrent from a byte[] containing the bencoded data
+ ///
+ /// The byte[] containing the data
+ ///
+ public static Torrent Load(byte[] data)
+ {
+ Check.Data(data);
+
+ using (MemoryStream s = new MemoryStream(data))
+ return Load(s, "");
+ }
+
+ ///
+ /// Loads a .torrent from the supplied stream
+ ///
+ /// The stream containing the data to load
+ ///
+ public static Torrent Load(Stream stream)
+ {
+ Check.Stream(stream);
+
+ if (stream == null)
+ throw new ArgumentNullException("stream");
+
+ return Torrent.Load(stream, "");
+ }
+
+ ///
+ /// Loads a .torrent file from the specified URL
+ ///
+ /// The URL to download the .torrent from
+ /// The path to download the .torrent to before it gets loaded
+ ///
+ public static Torrent Load(Uri url, string location)
+ {
+ Check.Url(url);
+ Check.Location(location);
+
+ try
+ {
+ using (WebClient client = new WebClient())
+ client.DownloadFile(url, location);
+ }
+ catch (Exception ex)
+ {
+ throw new TorrentException("Could not download .torrent file from the specified url", ex);
+ }
+
+ return Torrent.Load(location);
+ }
+
+ ///
+ /// Loads a .torrent from the specificed path. A return value indicates
+ /// whether the operation was successful.
+ ///
+ /// The path to load the .torrent file from
+ /// If the loading was succesful it is assigned the Torrent
+ /// True if successful
+ public static bool TryLoad(string path, out Torrent torrent)
+ {
+ Check.Path(path);
+
+ try
+ {
+ torrent = Torrent.Load(path);
+ }
+ catch
+ {
+ torrent = null;
+ }
+
+ return torrent != null;
+ }
+
+ ///
+ /// Loads a .torrent from the specified byte[]. A return value indicates
+ /// whether the operation was successful.
+ ///
+ /// The byte[] to load the .torrent from
+ /// If loading was successful, it contains the Torrent
+ /// True if successful
+ public static bool TryLoad(byte[] data, out Torrent torrent)
+ {
+ Check.Data(data);
+
+ try
+ {
+ torrent = Torrent.Load(data);
+ }
+ catch
+ {
+ torrent = null;
+ }
+
+ return torrent != null;
+ }
+
+ ///
+ /// Loads a .torrent from the supplied stream. A return value indicates
+ /// whether the operation was successful.
+ ///
+ /// The stream containing the data to load
+ /// If the loading was succesful it is assigned the Torrent
+ /// True if successful
+ public static bool TryLoad(Stream stream, out Torrent torrent)
+ {
+ Check.Stream(stream);
+
+ try
+ {
+ torrent = Torrent.Load(stream);
+ }
+ catch
+ {
+ torrent = null;
+ }
+
+ return torrent != null;
+ }
+
+ ///
+ /// Loads a .torrent file from the specified URL. A return value indicates
+ /// whether the operation was successful.
+ ///
+ /// The URL to download the .torrent from
+ /// The path to download the .torrent to before it gets loaded
+ /// If the loading was succesful it is assigned the Torrent
+ /// True if successful
+ public static bool TryLoad(Uri url, string location, out Torrent torrent)
+ {
+ Check.Url(url);
+ Check.Location(location);
+
+ try
+ {
+ torrent = Torrent.Load(url, location);
+ }
+ catch
+ {
+ torrent = null;
+ }
+
+ return torrent != null;
+ }
+
+ ///
+ /// Called from either Load(stream) or Load(string).
+ ///
+ ///
+ ///
+ ///
+ private static Torrent Load(Stream stream, string path)
+ {
+ Check.Stream(stream);
+ Check.Path(path);
+
+ try
+ {
+ Torrent t = Torrent.LoadCore ((BEncodedDictionary) BEncodedDictionary.Decode(stream));
+ t.torrentPath = path;
+ return t;
+ }
+ catch (BEncodingException ex)
+ {
+ throw new TorrentException("Invalid torrent file specified", ex);
+ }
+ }
+
+ public static Torrent Load(BEncodedDictionary torrentInformation)
+ {
+ return LoadCore ((BEncodedDictionary)BEncodedValue.Decode (torrentInformation.Encode ()));
+ }
+
+ internal static Torrent LoadCore(BEncodedDictionary torrentInformation)
+ {
+ Check.TorrentInformation(torrentInformation);
+
+ Torrent t = new Torrent();
+ t.LoadInternal(torrentInformation);
+
+ return t;
+ }
+
+ protected void LoadInternal(BEncodedDictionary torrentInformation)
+ {
+ Check.TorrentInformation(torrentInformation);
+ this.originalDictionary = torrentInformation;
+ this.torrentPath = "";
+
+ try
+ {
+ foreach (KeyValuePair keypair in torrentInformation)
+ {
+ switch (keypair.Key.Text)
+ {
+ case ("announce"):
+ // Ignore this if we have an announce-list
+ if (torrentInformation.ContainsKey("announce-list"))
+ break;
+ this.announceUrls.Add(new RawTrackerTier ());
+ this.announceUrls[0].Add(keypair.Value.ToString());
+ break;
+
+ case ("creation date"):
+ try
+ {
+ try
+ {
+ this.creationDate = this.creationDate.AddSeconds(long.Parse(keypair.Value.ToString()));
+ }
+ catch (Exception e)
+ {
+ if (e is ArgumentOutOfRangeException)
+ this.creationDate = this.creationDate.AddMilliseconds(long.Parse(keypair.Value.ToString()));
+ else
+ throw;
+ }
+ }
+ catch (Exception e)
+ {
+ if (e is ArgumentOutOfRangeException)
+ throw new BEncodingException("Argument out of range exception when adding seconds to creation date.", e);
+ else if (e is FormatException)
+ throw new BEncodingException(String.Format("Could not parse {0} into a number", keypair.Value), e);
+ else
+ throw;
+ }
+ break;
+
+ case ("nodes"):
+ this.nodes = (BEncodedList)keypair.Value;
+ break;
+
+ case ("comment.utf-8"):
+ if (keypair.Value.ToString().Length != 0)
+ this.comment = keypair.Value.ToString(); // Always take the UTF-8 version
+ break; // even if there's an existing value
+
+ case ("comment"):
+ if (String.IsNullOrEmpty(this.comment))
+ this.comment = keypair.Value.ToString();
+ break;
+
+ case ("publisher-url.utf-8"): // Always take the UTF-8 version
+ this.publisherUrl = keypair.Value.ToString(); // even if there's an existing value
+ break;
+
+ case ("publisher-url"):
+ if (String.IsNullOrEmpty(this.publisherUrl))
+ this.publisherUrl = keypair.Value.ToString();
+ break;
+
+ case ("azureus_properties"):
+ this.azureusProperties = keypair.Value;
+ break;
+
+ case ("created by"):
+ this.createdBy = keypair.Value.ToString();
+ break;
+
+ case ("encoding"):
+ this.encoding = keypair.Value.ToString();
+ break;
+
+ case ("info"):
+ using (SHA1 s = HashAlgoFactory.Create())
+ this.infoHash = new InfoHash (s.ComputeHash(keypair.Value.Encode()));
+ this.ProcessInfo(((BEncodedDictionary)keypair.Value));
+ break;
+
+ case ("name"): // Handled elsewhere
+ break;
+
+ case ("announce-list"):
+ if (keypair.Value is BEncodedString)
+ break;
+ BEncodedList announces = (BEncodedList)keypair.Value;
+
+ for (int j = 0; j < announces.Count; j++)
+ {
+ if (announces[j] is BEncodedList)
+ {
+ BEncodedList bencodedTier = (BEncodedList)announces[j];
+ List tier = new List(bencodedTier.Count);
+
+ for (int k = 0; k < bencodedTier.Count; k++)
+ tier.Add(bencodedTier[k].ToString());
+
+ Toolbox.Randomize(tier);
+
+ RawTrackerTier collection = new RawTrackerTier ();
+ for (int k = 0; k < tier.Count; k++)
+ collection.Add(tier[k]);
+
+ if (collection.Count != 0)
+ this.announceUrls.Add(collection);
+ }
+ else
+ {
+ throw new BEncodingException(String.Format("Non-BEncodedList found in announce-list (found {0})",
+ announces[j].GetType()));
+ }
+ }
+ break;
+
+ case ("httpseeds"):
+ // This form of web-seeding is not supported.
+ break;
+
+ case ("url-list"):
+ if (keypair.Value is BEncodedString)
+ {
+ this.getRightHttpSeeds.Add(((BEncodedString)keypair.Value).Text);
+ }
+ else if (keypair.Value is BEncodedList)
+ {
+ foreach (BEncodedString str in (BEncodedList)keypair.Value)
+ this.GetRightHttpSeeds.Add(str.Text);
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ if (e is BEncodingException)
+ throw;
+ else
+ throw new BEncodingException("", e);
+ }
+ }
+
+ #endregion Loading methods
+ }
+}
diff --git a/src/MonoTorrent/TorrentException.cs b/src/MonoTorrent/TorrentException.cs
new file mode 100644
index 000000000..05a0cc05d
--- /dev/null
+++ b/src/MonoTorrent/TorrentException.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace MonoTorrent
+{
+ [Serializable]
+ public class TorrentException : Exception
+ {
+ public TorrentException()
+ : base()
+ {
+ }
+
+ public TorrentException(string message)
+ : base(message)
+ {
+ }
+
+ public TorrentException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ public TorrentException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/src/MonoTorrent/TorrentFile.cs b/src/MonoTorrent/TorrentFile.cs
new file mode 100644
index 000000000..cff60141f
--- /dev/null
+++ b/src/MonoTorrent/TorrentFile.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Text;
+
+namespace MonoTorrent
+{
+ ///
+ /// This is the base class for the files available to download from within a .torrent.
+ /// This should be inherited by both Client and Tracker "TorrentFile" classes
+ ///
+ public class TorrentFile : IEquatable
+ {
+ #region Private Fields
+
+ private BitField bitfield;
+ private BitField selector;
+ private byte[] ed2k;
+ private int endPiece;
+ private string fullPath;
+ private long length;
+ private byte[] md5;
+ private string path;
+ private Priority priority;
+ private byte[] sha1;
+ private int startPiece;
+
+ #endregion Private Fields
+
+
+ #region Member Variables
+
+ ///
+ /// The number of pieces which have been successfully downloaded which are from this file
+ ///
+ public BitField BitField
+ {
+ get { return this.bitfield; }
+ }
+
+ public long BytesDownloaded
+ {
+ get { return (long)(this.BitField.PercentComplete * this.Length / 100.0); }
+ }
+
+ ///
+ /// The ED2K hash of the file
+ ///
+ public byte[] ED2K
+ {
+ get { return this.ed2k; }
+ }
+
+ ///
+ /// The index of the last piece of this file
+ ///
+ public int EndPieceIndex
+ {
+ get { return this.endPiece; }
+ }
+
+ public string FullPath
+ {
+ get { return this.fullPath; }
+ internal set { this.fullPath = value; }
+ }
+
+ ///
+ /// The length of the file in bytes
+ ///
+ public long Length
+ {
+ get { return this.length; }
+ }
+
+ ///
+ /// The MD5 hash of the file
+ ///
+ public byte[] MD5
+ {
+ get { return this.md5; }
+ internal set { this.md5 = value; }
+ }
+
+ ///
+ /// In the case of a single torrent file, this is the name of the file.
+ /// In the case of a multi-file torrent this is the relative path of the file
+ /// (including the filename) from the base directory
+ ///
+ public string Path
+ {
+ get { return this.path; }
+ }
+
+ ///
+ /// The priority of this torrent file
+ ///
+ public Priority Priority
+ {
+ get { return this.priority; }
+ set { this.priority = value; }
+ }
+
+ ///
+ /// The SHA1 hash of the file
+ ///
+ public byte[] SHA1
+ {
+ get { return this.sha1; }
+ }
+
+ ///
+ /// The index of the first piece of this file
+ ///
+ public int StartPieceIndex
+ {
+ get { return this.startPiece; }
+ }
+
+ #endregion
+
+
+ #region Constructors
+ public TorrentFile(string path, long length)
+ : this(path, length, path)
+ {
+
+ }
+
+ public TorrentFile (string path, long length, string fullPath)
+ : this (path, length, fullPath, 0, 0)
+ {
+
+ }
+
+ public TorrentFile (string path, long length, int startIndex, int endIndex)
+ : this (path, length, path, startIndex, endIndex)
+ {
+
+ }
+
+ public TorrentFile(string path, long length, string fullPath, int startIndex, int endIndex)
+ : this(path, length, fullPath, startIndex, endIndex, null, null, null)
+ {
+
+ }
+
+ public TorrentFile(string path, long length, string fullPath, int startIndex, int endIndex, byte[] md5, byte[] ed2k, byte[] sha1)
+ {
+ this.bitfield = new BitField(endIndex - startIndex + 1);
+ this.ed2k = ed2k;
+ this.endPiece = endIndex;
+ this.fullPath = fullPath;
+ this.length = length;
+ this.md5 = md5;
+ this.path = path;
+ this.priority = Priority.Normal;
+ this.sha1 = sha1;
+ this.startPiece = startIndex;
+ }
+
+ #endregion
+
+
+ #region Methods
+
+ public override bool Equals(object obj)
+ {
+ return this.Equals(obj as TorrentFile);
+ }
+
+ public bool Equals(TorrentFile other)
+ {
+ return other == null ? false : this.path == other.path && this.length == other.length; ;
+ }
+
+ public override int GetHashCode()
+ {
+ return this.path.GetHashCode();
+ }
+
+ internal BitField GetSelector(int totalPieces)
+ {
+ if (this.selector != null)
+ return this.selector;
+
+ this.selector = new BitField(totalPieces);
+ for (int i = this.StartPieceIndex; i <= this.EndPieceIndex; i++)
+ this.selector[i] = true;
+ return this.selector;
+ }
+
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder(32);
+ sb.Append("File: ");
+ sb.Append(this.path);
+ sb.Append(" StartIndex: ");
+ sb.Append(this.StartPieceIndex);
+ sb.Append(" EndIndex: ");
+ sb.Append(this.EndPieceIndex);
+ return sb.ToString();
+ }
+
+ #endregion Methods
+ }
+}
\ No newline at end of file
diff --git a/src/MonoTorrent/UriHelper.cs b/src/MonoTorrent/UriHelper.cs
new file mode 100644
index 000000000..d806e596a
--- /dev/null
+++ b/src/MonoTorrent/UriHelper.cs
@@ -0,0 +1,153 @@
+//
+// System.Web.HttpUtility/HttpEncoder
+//
+// Authors:
+// Patrik Torstensson (Patrik.Torstensson@labs2.com)
+// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se)
+// Tim Coleman (tim@timcoleman.com)
+using System;
+using System.Text;
+using System.IO;
+using System.Collections.Generic;
+
+namespace MonoTorrent
+{
+ static class UriHelper
+ {
+ static readonly char [] hexChars = "0123456789abcdef".ToCharArray ();
+
+ public static string UrlEncode (byte[] bytes)
+ {
+ if (bytes == null)
+ throw new ArgumentNullException ("bytes");
+
+ var result = new MemoryStream (bytes.Length);
+ for (int i = 0; i < bytes.Length; i++)
+ UrlEncodeChar ((char)bytes [i], result, false);
+
+ return Encoding.ASCII.GetString (result.ToArray());
+ }
+
+ public static byte [] UrlDecode (string s)
+ {
+ if (null == s)
+ return null;
+
+ var e = Encoding.UTF8;
+ if (s.IndexOf ('%') == -1 && s.IndexOf ('+') == -1)
+ return e.GetBytes (s);
+
+ long len = s.Length;
+ var bytes = new List ();
+ int xchar;
+ char ch;
+
+ for (int i = 0; i < len; i++) {
+ ch = s [i];
+ if (ch == '%' && i + 2 < len && s [i + 1] != '%') {
+ if (s [i + 1] == 'u' && i + 5 < len) {
+ // unicode hex sequence
+ xchar = GetChar (s, i + 2, 4);
+ if (xchar != -1) {
+ WriteCharBytes (bytes, (char)xchar, e);
+ i += 5;
+ } else
+ WriteCharBytes (bytes, '%', e);
+ } else if ((xchar = GetChar (s, i + 1, 2)) != -1) {
+ WriteCharBytes (bytes, (char)xchar, e);
+ i += 2;
+ } else {
+ WriteCharBytes (bytes, '%', e);
+ }
+ continue;
+ }
+
+ if (ch == '+')
+ WriteCharBytes (bytes, ' ', e);
+ else
+ WriteCharBytes (bytes, ch, e);
+ }
+
+ return bytes.ToArray ();
+ }
+
+ static void UrlEncodeChar (char c, Stream result, bool isUnicode) {
+ if (c > ' ' && NotEncoded (c)) {
+ result.WriteByte ((byte)c);
+ return;
+ }
+ if (c==' ') {
+ result.WriteByte ((byte)'+');
+ return;
+ }
+ if ( (c < '0') ||
+ (c < 'A' && c > '9') ||
+ (c > 'Z' && c < 'a') ||
+ (c > 'z')) {
+ if (isUnicode && c > 127) {
+ result.WriteByte ((byte)'%');
+ result.WriteByte ((byte)'u');
+ result.WriteByte ((byte)'0');
+ result.WriteByte ((byte)'0');
+ }
+ else
+ result.WriteByte ((byte)'%');
+
+ int idx = ((int) c) >> 4;
+ result.WriteByte ((byte)hexChars [idx]);
+ idx = ((int) c) & 0x0F;
+ result.WriteByte ((byte)hexChars [idx]);
+ }
+ else {
+ result.WriteByte ((byte)c);
+ }
+ }
+
+ static int GetChar (string str, int offset, int length)
+ {
+ int val = 0;
+ int end = length + offset;
+ for (int i = offset; i < end; i++) {
+ char c = str [i];
+ if (c > 127)
+ return -1;
+
+ int current = GetInt ((byte) c);
+ if (current == -1)
+ return -1;
+ val = (val << 4) + current;
+ }
+
+ return val;
+ }
+
+ static int GetInt (byte b)
+ {
+ char c = (char) b;
+ if (c >= '0' && c <= '9')
+ return c - '0';
+
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+
+ return -1;
+ }
+
+ static bool NotEncoded (char c)
+ {
+ return c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_' || c == '\'';
+ }
+
+ static void WriteCharBytes (List buf, char ch, Encoding e)
+ {
+ if (ch > 255) {
+ foreach (byte b in e.GetBytes (new char[] { ch }))
+ buf.Add (b);
+ } else
+ buf.Add ((byte)ch);
+ }
+ }
+}
diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs
index 23eeef97c..f5724489c 100644
--- a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs
+++ b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs
@@ -19,5 +19,6 @@ namespace NzbDrone.Api.Config
public String ChownGroup { get; set; }
public Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
+ public Boolean CopyUsingHardlinks { get; set; }
}
}
diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs
index dd463e1ac..6f36cf59b 100644
--- a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs
+++ b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs
@@ -162,6 +162,72 @@ namespace NzbDrone.Common.Test.DiskProviderTests
Directory.Exists(sourceDir).Should().BeFalse();
}
+ [Test]
+ public void should_be_able_to_hardlink_file()
+ {
+ var sourceDir = GetTempFilePath();
+ var source = Path.Combine(sourceDir, "test.txt");
+ var destination = Path.Combine(sourceDir, "destination.txt");
+
+ Directory.CreateDirectory(sourceDir);
+
+ Subject.WriteAllText(source, "SourceFile");
+
+ var result = Subject.TransferFile(source, destination, TransferMode.HardLink);
+
+ result.Should().Be(TransferMode.HardLink);
+
+ File.AppendAllText(source, "Test");
+ File.ReadAllText(destination).Should().Be("SourceFileTest");
+ }
+
+ private void DoHardLinkRename(FileShare fileShare)
+ {
+ var sourceDir = GetTempFilePath();
+ var source = Path.Combine(sourceDir, "test.txt");
+ var destination = Path.Combine(sourceDir, "destination.txt");
+ var rename = Path.Combine(sourceDir, "rename.txt");
+
+ Directory.CreateDirectory(sourceDir);
+
+ Subject.WriteAllText(source, "SourceFile");
+
+ Subject.TransferFile(source, destination, TransferMode.HardLink);
+
+ using (var stream = new FileStream(source, FileMode.Open, FileAccess.Read, fileShare))
+ {
+ stream.ReadByte();
+
+ Subject.MoveFile(destination, rename);
+
+ stream.ReadByte();
+ }
+
+ File.Exists(rename).Should().BeTrue();
+ File.Exists(destination).Should().BeFalse();
+
+ File.AppendAllText(source, "Test");
+ File.ReadAllText(rename).Should().Be("SourceFileTest");
+ }
+
+ [Test]
+ public void should_be_able_to_rename_open_hardlinks_with_fileshare_delete()
+ {
+ DoHardLinkRename(FileShare.Delete);
+ }
+
+ [Test]
+ public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_none()
+ {
+ Assert.Throws(() => DoHardLinkRename(FileShare.None));
+ }
+
+ [Test]
+ public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_write()
+ {
+ Assert.Throws(() => DoHardLinkRename(FileShare.Read));
+ }
+
[Test]
public void empty_folder_should_return_folder_modified_date()
{
diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs
index e78d7b70f..069652ed6 100644
--- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs
+++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs
@@ -11,12 +11,16 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestFixture]
public class CleanseLogMessageFixture
{
- [TestCase(@"http://127.0.0.1:1234/api/call?vv=1&apikey=mySecret")]
- [TestCase(@"http://127.0.0.1:1234/api/call?vv=1&ma_username=mySecret&ma_password=mySecret")]
+ // Indexer Urls
+ [TestCase(@"https://iptorrents.com/torrents/rss?u=mySecret;tp=mySecret;l5;download")]
+ [TestCase(@"http://rss.torrentleech.org/mySecret")]
+ [TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
[TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")]
// Sabnzbd
+ [TestCase(@"http://127.0.0.1:1234/api/call?vv=1&apikey=mySecret")]
+ [TestCase(@"http://127.0.0.1:1234/api/call?vv=1&ma_username=mySecret&ma_password=mySecret")]
[TestCase(@"""config"":{""newzbin"":{""username"":""mySecret"",""password"":""mySecret""}")]
[TestCase(@"""nzbxxx"":{""username"":""mySecret"",""apikey"":""mySecret""}")]
[TestCase(@"""growl"":{""growl_password"":""mySecret"",""growl_server"":""""}")]
@@ -24,6 +28,19 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"""misc"":{""username"":""mySecret"",""api_key"":""mySecret"",""password"":""mySecret"",""nzb_key"":""mySecret""}")]
[TestCase(@"""servers"":[{""username"":""mySecret"",""password"":""mySecret""}]")]
[TestCase(@"""misc"":{""email_account"":""mySecret"",""email_to"":[],""email_from"":"""",""email_pwd"":""mySecret""}")]
+ // uTorrent
+ [TestCase(@"http://localhost:9091/gui/?token=wThmph5l0ZXfH-a6WOA4lqiLvyjCP0FpMrMeXmySecret_VXBO11HoKL751MAAAAA&list=1")]
+ [TestCase(@",[""boss_key"",0,""mySecret"",{""access"":""Y""}],[""boss_key_salt"",0,""mySecret"",{""access"":""W""}]")]
+ [TestCase(@",[""webui.username"",2,""mySecret"",{""access"":""Y""}],[""webui.password"",2,""mySecret"",{""access"":""Y""}]")]
+ [TestCase(@",[""webui.uconnect_username"",2,""mySecret"",{""access"":""Y""}],[""webui.uconnect_password"",2,""mySecret"",{""access"":""Y""}]")]
+ [TestCase(@",[""proxy.proxy"",2,""mySecret"",{""access"":""Y""}]")]
+ [TestCase(@",[""proxy.username"",2,""mySecret"",{""access"":""Y""}],[""proxy.password"",2,""mySecret"",{""access"":""Y""}]")]
+ // Deluge
+ [TestCase(@",{""download_location"": ""C:\Users\\mySecret mySecret\\Downloads""}")]
+ [TestCase(@",{""download_location"": ""/home/mySecret/Downloads""}")]
+ // BroadcastheNet
+ [TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
+ [TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
public void should_clean_message(String message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj
index f2e2d3429..a4199d64b 100644
--- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj
+++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj
@@ -80,6 +80,7 @@
+
diff --git a/src/NzbDrone.Common.Test/OsPathFixture.cs b/src/NzbDrone.Common.Test/OsPathFixture.cs
new file mode 100644
index 000000000..1e58b7eb3
--- /dev/null
+++ b/src/NzbDrone.Common.Test/OsPathFixture.cs
@@ -0,0 +1,237 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using NzbDrone.Common.Disk;
+using NzbDrone.Test.Common;
+using FluentAssertions;
+
+namespace NzbDrone.Common.Test
+{
+ public class OsPathFixture : TestBase
+ {
+ [TestCase(@"C:\rooted\windows\path\", OsPathKind.Windows)]
+ [TestCase(@"C:\rooted\windows\path", OsPathKind.Windows)]
+ [TestCase(@"C:\", OsPathKind.Windows)]
+ [TestCase(@"C:", OsPathKind.Windows)]
+ [TestCase(@"\\rooted\unc\path\", OsPathKind.Windows)]
+ [TestCase(@"\\rooted\unc\path", OsPathKind.Windows)]
+ [TestCase(@"\relative\windows\path\", OsPathKind.Windows)]
+ [TestCase(@"\relative\windows\path", OsPathKind.Windows)]
+ [TestCase(@"relative\windows\path\", OsPathKind.Windows)]
+ [TestCase(@"relative\windows\path", OsPathKind.Windows)]
+ [TestCase(@"relative\", OsPathKind.Windows)]
+ [TestCase(@"relative", OsPathKind.Unknown)]
+ [TestCase("/rooted/linux/path/", OsPathKind.Unix)]
+ [TestCase("/rooted/linux/path", OsPathKind.Unix)]
+ [TestCase("/", OsPathKind.Unix)]
+ [TestCase("linux/path", OsPathKind.Unix)]
+ public void should_auto_detect_kind(String path, OsPathKind kind)
+ {
+ var result = new OsPath(path);
+
+ result.Kind.Should().Be(kind);
+
+ if (kind == OsPathKind.Windows)
+ {
+ result.IsWindowsPath.Should().BeTrue();
+ result.IsUnixPath.Should().BeFalse();
+ }
+ else if (kind == OsPathKind.Unix)
+ {
+ result.IsWindowsPath.Should().BeFalse();
+ result.IsUnixPath.Should().BeTrue();
+ }
+ else
+ {
+ result.IsWindowsPath.Should().BeFalse();
+ result.IsUnixPath.Should().BeFalse();
+ }
+ }
+
+ [Test]
+ public void should_add_directory_slash()
+ {
+ var osPath = new OsPath(@"C:\rooted\windows\path\");
+
+ osPath.Directory.Should().NotBeNull();
+ osPath.Directory.ToString().Should().Be(@"C:\rooted\windows\");
+ }
+
+ [TestCase(@"C:\rooted\windows\path", @"C:\rooted\windows\")]
+ [TestCase(@"C:\rooted", @"C:\")]
+ [TestCase(@"C:", null)]
+ [TestCase("/rooted/linux/path", "/rooted/linux/")]
+ [TestCase("/rooted", "/")]
+ [TestCase("/", null)]
+ public void should_return_parent_directory(String path, String expectedParent)
+ {
+ var osPath = new OsPath(path);
+
+ osPath.Directory.Should().NotBeNull();
+ osPath.Directory.Should().Be(new OsPath(expectedParent));
+ }
+
+ [Test]
+ public void should_return_empty_as_parent_of_root_unc()
+ {
+ var osPath = new OsPath(@"\\unc");
+
+ osPath.Directory.IsEmpty.Should().BeTrue();
+ }
+
+ [TestCase(@"C:\rooted\windows\path")]
+ [TestCase(@"C:")]
+ [TestCase(@"\\blaat")]
+ [TestCase("/rooted/linux/path")]
+ [TestCase("/")]
+ public void should_detect_rooted_ospaths(String path)
+ {
+ var osPath = new OsPath(path);
+
+ osPath.IsRooted.Should().BeTrue();
+ }
+
+ [TestCase(@"\rooted\windows\path")]
+ [TestCase(@"rooted\windows\path")]
+ [TestCase(@"path")]
+ [TestCase("linux/path")]
+ public void should_detect_unrooted_ospaths(String path)
+ {
+ var osPath = new OsPath(path);
+
+ osPath.IsRooted.Should().BeFalse();
+ }
+
+ [TestCase(@"C:\rooted\windows\path", "path")]
+ [TestCase(@"C:", "C:")]
+ [TestCase(@"\\blaat", "blaat")]
+ [TestCase("/rooted/linux/path", "path")]
+ [TestCase("/", null)]
+ [TestCase(@"\rooted\windows\path\", "path")]
+ [TestCase(@"rooted\windows\path", "path")]
+ [TestCase(@"path", "path")]
+ [TestCase("linux/path", "path")]
+ public void should_return_filename(String path, String expectedFilePath)
+ {
+ var osPath = new OsPath(path);
+
+ osPath.FileName.Should().Be(expectedFilePath);
+ }
+
+ [Test]
+ public void should_compare_windows_ospathkind_case_insensitive()
+ {
+ var left = new OsPath(@"C:\rooted\Windows\path");
+ var right = new OsPath(@"C:\rooted\windows\path");
+
+ left.Should().Be(right);
+ }
+
+ [Test]
+ public void should_compare_unix_ospathkind_case_sensitive()
+ {
+ var left = new OsPath(@"/rooted/Linux/path");
+ var right = new OsPath(@"/rooted/linux/path");
+
+ left.Should().NotBe(right);
+ }
+
+ [Test]
+ public void should_not_ignore_trailing_slash_during_compare()
+ {
+ var left = new OsPath(@"/rooted/linux/path/");
+ var right = new OsPath(@"/rooted/linux/path");
+
+ left.Should().NotBe(right);
+ }
+
+ [TestCase(@"C:\Test", @"sub", @"C:\Test\sub")]
+ [TestCase(@"C:\Test", @"sub\test", @"C:\Test\sub\test")]
+ [TestCase(@"C:\Test\", @"\sub", @"C:\Test\sub")]
+ [TestCase(@"C:\Test", @"sub\", @"C:\Test\sub\")]
+ [TestCase(@"C:\Test", @"C:\Test2\sub", @"C:\Test2\sub")]
+ [TestCase(@"/Test", @"sub", @"/Test/sub")]
+ [TestCase(@"/Test", @"sub/", @"/Test/sub/")]
+ [TestCase(@"/Test", @"sub/", @"/Test/sub/")]
+ [TestCase(@"/Test/", @"sub/test/", @"/Test/sub/test/")]
+ [TestCase(@"/Test/", @"/Test2/", @"/Test2/")]
+ [TestCase(@"C:\Test", "", @"C:\Test")]
+ public void should_combine_path(String left, String right, String expectedResult)
+ {
+ var osPathLeft = new OsPath(left);
+ var osPathRight = new OsPath(right);
+
+ var result = osPathLeft + osPathRight;
+
+ result.FullPath.Should().Be(expectedResult);
+ }
+
+ [Test]
+ public void should_fix_slashes_windows()
+ {
+ var osPath = new OsPath(@"C:/on/windows/transmission\uses/forward/slashes");
+
+ osPath.Kind.Should().Be(OsPathKind.Windows);
+ osPath.FullPath.Should().Be(@"C:\on\windows\transmission\uses\forward\slashes");
+ }
+
+ [Test]
+ public void should_fix_slashes_unix()
+ {
+ var osPath = new OsPath(@"/just/a/test\to\verify the/slashes\");
+
+ osPath.Kind.Should().Be(OsPathKind.Unix);
+ osPath.FullPath.Should().Be(@"/just/a/test/to/verify the/slashes/");
+ }
+
+ [Test]
+ public void should_combine_mixed_slashes()
+ {
+ var left = new OsPath(@"C:/on/windows/transmission");
+ var right = new OsPath(@"uses/forward/slashes", OsPathKind.Unknown);
+
+ var osPath = left + right;
+
+ osPath.Kind.Should().Be(OsPathKind.Windows);
+ osPath.FullPath.Should().Be(@"C:\on\windows\transmission\uses\forward\slashes");
+ }
+
+ [TestCase(@"C:\Test\Data\", @"C:\Test\Data\Sub\Folder", @"Sub\Folder")]
+ [TestCase(@"C:\Test\Data\", @"C:\Test\Data2\Sub\Folder", @"..\Data2\Sub\Folder")]
+ [TestCase(@"/parent/folder", @"/parent/folder/Sub/Folder", @"Sub/Folder")]
+ public void should_create_relative_path(String parent, String child, String expected)
+ {
+ var left = new OsPath(child);
+ var right = new OsPath(parent);
+
+ var osPath = left - right;
+
+ osPath.Kind.Should().Be(OsPathKind.Unknown);
+ osPath.FullPath.Should().Be(expected);
+ }
+
+ [Test]
+ public void should_parse_null_as_empty()
+ {
+ var result = new OsPath(null);
+
+ result.FullPath.Should().BeEmpty();
+ result.IsEmpty.Should().BeTrue();
+ }
+
+ [TestCase(@"C:\Test\", @"C:\Test", true)]
+ [TestCase(@"C:\Test\", @"C:\Test\Contains\", true)]
+ [TestCase(@"C:\Test\", @"C:\Other\", false)]
+ public void should_evaluate_contains(String parent, String child, Boolean expectedResult)
+ {
+ var left = new OsPath(parent);
+ var right = new OsPath(child);
+
+ var result = left.Contains(right);
+
+ result.Should().Be(expectedResult);
+ }
+ }
+}
diff --git a/src/NzbDrone.Common/DictionaryExtensions.cs b/src/NzbDrone.Common/DictionaryExtensions.cs
index 22c6184ea..8c384fede 100644
--- a/src/NzbDrone.Common/DictionaryExtensions.cs
+++ b/src/NzbDrone.Common/DictionaryExtensions.cs
@@ -23,5 +23,10 @@ namespace NzbDrone.Common
return merged;
}
+
+ public static void Add(this ICollection> collection, TKey key, TValue value)
+ {
+ collection.Add(key, value);
+ }
}
}
diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs
index 8e8fbe0a9..e1ef09c0f 100644
--- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs
+++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs
@@ -13,12 +13,6 @@ namespace NzbDrone.Common.Disk
{
public abstract class DiskProviderBase : IDiskProvider
{
- enum TransferAction
- {
- Copy,
- Move
- }
-
private static readonly Logger Logger = NzbDroneLogger.GetLogger();
public abstract long? GetAvailableSpace(string path);
@@ -152,7 +146,7 @@ namespace NzbDrone.Common.Disk
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
- TransferFolder(source, destination, TransferAction.Copy);
+ TransferFolder(source, destination, TransferMode.Copy);
}
public void MoveFolder(string source, string destination)
@@ -162,7 +156,7 @@ namespace NzbDrone.Common.Disk
try
{
- TransferFolder(source, destination, TransferAction.Move);
+ TransferFolder(source, destination, TransferMode.Move);
DeleteFolder(source, true);
}
catch (Exception e)
@@ -173,15 +167,15 @@ namespace NzbDrone.Common.Disk
}
}
- private void TransferFolder(string source, string target, TransferAction transferAction)
+ public void TransferFolder(string source, string destination, TransferMode mode)
{
Ensure.That(source, () => source).IsValidPath();
- Ensure.That(target, () => target).IsValidPath();
+ Ensure.That(destination, () => destination).IsValidPath();
- Logger.ProgressDebug("{0} {1} -> {2}", transferAction, source, target);
+ Logger.ProgressDebug("{0} {1} -> {2}", mode, source, destination);
var sourceFolder = new DirectoryInfo(source);
- var targetFolder = new DirectoryInfo(target);
+ var targetFolder = new DirectoryInfo(destination);
if (!targetFolder.Exists)
{
@@ -190,28 +184,16 @@ namespace NzbDrone.Common.Disk
foreach (var subDir in sourceFolder.GetDirectories())
{
- TransferFolder(subDir.FullName, Path.Combine(target, subDir.Name), transferAction);
+ TransferFolder(subDir.FullName, Path.Combine(destination, subDir.Name), mode);
}
foreach (var sourceFile in sourceFolder.GetFiles("*.*", SearchOption.TopDirectoryOnly))
{
- var destFile = Path.Combine(target, sourceFile.Name);
+ var destFile = Path.Combine(destination, sourceFile.Name);
- Logger.ProgressDebug("{0} {1} -> {2}", transferAction, sourceFile, destFile);
+ Logger.ProgressDebug("{0} {1} -> {2}", mode, sourceFile, destFile);
- switch (transferAction)
- {
- case TransferAction.Copy:
- {
- sourceFile.CopyTo(destFile, true);
- break;
- }
- case TransferAction.Move:
- {
- MoveFile(sourceFile.FullName, destFile, true);
- break;
- }
- }
+ TransferFile(sourceFile.FullName, destFile, mode, true);
}
}
@@ -227,19 +209,15 @@ namespace NzbDrone.Common.Disk
public void CopyFile(string source, string destination, bool overwrite = false)
{
- Ensure.That(source, () => source).IsValidPath();
- Ensure.That(destination, () => destination).IsValidPath();
-
- if (source.PathEquals(destination))
- {
- Logger.Warn("Source and destination can't be the same {0}", source);
- return;
- }
-
- File.Copy(source, destination, overwrite);
+ TransferFile(source, destination, TransferMode.Copy, overwrite);
}
public void MoveFile(string source, string destination, bool overwrite = false)
+ {
+ TransferFile(source, destination, TransferMode.Move, overwrite);
+ }
+
+ public TransferMode TransferFile(string source, string destination, TransferMode mode, bool overwrite)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
@@ -247,7 +225,7 @@ namespace NzbDrone.Common.Disk
if (source.PathEquals(destination))
{
Logger.Warn("Source and destination can't be the same {0}", source);
- return;
+ return TransferMode.None;
}
if (FileExists(destination) && overwrite)
@@ -255,10 +233,37 @@ namespace NzbDrone.Common.Disk
DeleteFile(destination);
}
- RemoveReadOnly(source);
- File.Move(source, destination);
+ if (mode.HasFlag(TransferMode.HardLink))
+ {
+ bool createdHardlink = TryCreateHardLink(source, destination);
+ if (createdHardlink)
+ {
+ return TransferMode.HardLink;
+ }
+ else if (!mode.HasFlag(TransferMode.Copy))
+ {
+ throw new IOException("Hardlinking from '" + source + "' to '" + destination + "' failed.");
+ }
+ }
+
+ if (mode.HasFlag(TransferMode.Copy))
+ {
+ File.Copy(source, destination, overwrite);
+ return TransferMode.Copy;
+ }
+
+ if (mode.HasFlag(TransferMode.Move))
+ {
+ RemoveReadOnly(source);
+ File.Move(source, destination);
+ return TransferMode.Move;
+ }
+
+ return TransferMode.None;
}
+ public abstract bool TryCreateHardLink(string source, string destination);
+
public void DeleteFolder(string path, bool recursive)
{
Ensure.That(path, () => path).IsValidPath();
diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs
index 1912b02ee..cc9934019 100644
--- a/src/NzbDrone.Common/Disk/IDiskProvider.cs
+++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs
@@ -25,9 +25,12 @@ namespace NzbDrone.Common.Disk
void CreateFolder(string path);
void CopyFolder(string source, string destination);
void MoveFolder(string source, string destination);
+ void TransferFolder(string source, string destination, TransferMode transferMode);
void DeleteFile(string path);
void CopyFile(string source, string destination, bool overwrite = false);
void MoveFile(string source, string destination, bool overwrite = false);
+ TransferMode TransferFile(string source, string destination, TransferMode transferMode, bool overwrite = false);
+ bool TryCreateHardLink(string source, string destination);
void DeleteFolder(string path, bool recursive);
string ReadAllText(string filePath);
void WriteAllText(string filename, string contents);
diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs
new file mode 100644
index 000000000..540f70c25
--- /dev/null
+++ b/src/NzbDrone.Common/Disk/OsPath.cs
@@ -0,0 +1,406 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NzbDrone.Common.Exceptions;
+
+namespace NzbDrone.Common.Disk
+{
+ public struct OsPath : IEquatable
+ {
+ private readonly String _path;
+ private readonly OsPathKind _kind;
+
+ public OsPath(String path)
+ {
+ if (path == null)
+ {
+ _kind = OsPathKind.Unknown;
+ _path = String.Empty;
+ }
+ else
+ {
+ _kind = DetectPathKind(path);
+ _path = FixSlashes(path, _kind);
+ }
+ }
+
+ public OsPath(String path, OsPathKind kind)
+ {
+ if (path == null)
+ {
+ _kind = kind;
+ _path = String.Empty;
+ }
+ else
+ {
+ _kind = kind;
+ _path = FixSlashes(path, kind);
+ }
+ }
+
+ private static OsPathKind DetectPathKind(String path)
+ {
+ if (path.StartsWith("/"))
+ {
+ return OsPathKind.Unix;
+ }
+ if (path.Contains(':') || path.Contains('\\'))
+ {
+ return OsPathKind.Windows;
+ }
+ else if (path.Contains('/'))
+ {
+ return OsPathKind.Unix;
+ }
+ else
+ {
+ return OsPathKind.Unknown;
+ }
+ }
+
+ private static String FixSlashes(String path, OsPathKind kind)
+ {
+ if (kind == OsPathKind.Windows)
+ {
+ return path.Replace('/', '\\');
+ }
+ else if (kind == OsPathKind.Unix)
+ {
+ return path.Replace('\\', '/');
+ }
+
+ return path;
+ }
+
+ public OsPathKind Kind
+ {
+ get { return _kind; }
+ }
+
+ public Boolean IsWindowsPath
+ {
+ get { return _kind == OsPathKind.Windows; }
+ }
+
+ public Boolean IsUnixPath
+ {
+ get { return _kind == OsPathKind.Unix; }
+ }
+
+ public Boolean IsEmpty
+ {
+ get
+ {
+ return _path.IsNullOrWhiteSpace();
+ }
+ }
+
+ public Boolean IsRooted
+ {
+ get
+ {
+ if (IsWindowsPath)
+ {
+ return _path.StartsWith(@"\\") || _path.Contains(':');
+ }
+ else if (IsUnixPath)
+ {
+ return _path.StartsWith("/");
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ public OsPath Directory
+ {
+ get
+ {
+ var index = GetFileNameIndex();
+
+ if (index == -1)
+ {
+ return new OsPath(null);
+ }
+ else
+ {
+ return new OsPath(_path.Substring(0, index), _kind).AsDirectory();
+ }
+ }
+ }
+
+ public String FullPath
+ {
+ get
+ {
+ return _path;
+ }
+ }
+
+ public String FileName
+ {
+ get
+ {
+ var index = GetFileNameIndex();
+
+ if (index == -1)
+ {
+ var path = _path.Trim('\\', '/');
+
+ if (path.Length == 0)
+ {
+ return null;
+ }
+
+ return path;
+ }
+ else
+ {
+ return _path.Substring(index).Trim('\\', '/');
+ }
+ }
+ }
+
+ private Int32 GetFileNameIndex()
+ {
+ if (_path.Length < 2)
+ {
+ return -1;
+ }
+
+ var index = _path.LastIndexOfAny(new[] { '/', '\\' }, _path.Length - 2);
+
+ if (index == -1)
+ {
+ return -1;
+ }
+
+ if (_path.StartsWith(@"\\") && index < 2)
+ {
+ return -1;
+ }
+
+ if (_path.StartsWith("/") && index == 0)
+ {
+ index++;
+ }
+
+ return index;
+ }
+
+ private String[] GetFragments()
+ {
+ return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ public override String ToString()
+ {
+ return _path;
+ }
+
+ public override Int32 GetHashCode()
+ {
+ return _path.ToLowerInvariant().GetHashCode();
+ }
+
+ public override Boolean Equals(Object obj)
+ {
+ if (obj is OsPath)
+ {
+ return Equals((OsPath)obj);
+ }
+ else if (obj is String)
+ {
+ return Equals(new OsPath(obj as String));
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public OsPath AsDirectory()
+ {
+ if (IsEmpty)
+ {
+ return this;
+ }
+
+ if (Kind == OsPathKind.Windows)
+ {
+ return new OsPath(_path.TrimEnd('\\') + "\\", _kind);
+ }
+ else if (Kind == OsPathKind.Unix)
+ {
+ return new OsPath(_path.TrimEnd('/') + "/", _kind);
+ }
+ else
+ {
+ return this;
+ }
+ }
+
+ public Boolean Contains(OsPath other)
+ {
+ if (!IsRooted || !other.IsRooted)
+ {
+ return false;
+ }
+
+ var leftFragments = GetFragments();
+ var rightFragments = other.GetFragments();
+
+ if (rightFragments.Length < leftFragments.Length)
+ {
+ return false;
+ }
+
+ var stringComparison = (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
+
+ for (int i = 0; i < leftFragments.Length; i++)
+ {
+ if (!String.Equals(leftFragments[i], rightFragments[i], stringComparison))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public Boolean Equals(OsPath other)
+ {
+ if (ReferenceEquals(other, null)) return false;
+
+ if (_path == other._path)
+ {
+ return true;
+ }
+
+ var left = _path;
+ var right = other._path;
+
+ if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows)
+ {
+ return String.Equals(left, right, StringComparison.InvariantCultureIgnoreCase);
+ }
+ else
+ {
+ return String.Equals(left, right, StringComparison.InvariantCulture);
+ }
+ }
+
+ public static Boolean operator ==(OsPath left, OsPath right)
+ {
+ if (ReferenceEquals(left, null)) return ReferenceEquals(right, null);
+
+ return left.Equals(right);
+ }
+
+ public static Boolean operator !=(OsPath left, OsPath right)
+ {
+ if (ReferenceEquals(left, null)) return !ReferenceEquals(right, null);
+
+ return !left.Equals(right);
+ }
+
+ public static OsPath operator +(OsPath left, OsPath right)
+ {
+ if (left.Kind != right.Kind && right.Kind != OsPathKind.Unknown)
+ {
+ throw new Exception(String.Format("Cannot combine OsPaths of different platforms ('{0}' + '{1}')", left, right));
+ }
+
+ if (right.IsEmpty)
+ {
+ return left;
+ }
+
+ if (right.IsRooted)
+ {
+ return right;
+ }
+
+ if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows)
+ {
+ return new OsPath(String.Join("\\", left._path.TrimEnd('\\'), right._path.TrimStart('\\')), OsPathKind.Windows);
+ }
+ else if (left.Kind == OsPathKind.Unix || right.Kind == OsPathKind.Unix)
+ {
+ return new OsPath(String.Join("/", left._path.TrimEnd('/'), right._path), OsPathKind.Unix);
+ }
+ else
+ {
+ return new OsPath(String.Join("/", left._path, right._path), OsPathKind.Unknown);
+ }
+ }
+
+ public static OsPath operator +(OsPath left, String right)
+ {
+ return left + new OsPath(right);
+ }
+
+ public static OsPath operator -(OsPath left, OsPath right)
+ {
+ if (!left.IsRooted || !right.IsRooted)
+ {
+ throw new ArgumentException("Cannot determine relative path for unrooted paths.");
+ }
+
+ var leftFragments = left.GetFragments();
+ var rightFragments = right.GetFragments();
+
+ var stringComparison = (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
+
+ int i;
+ for (i = 0; i < leftFragments.Length && i < rightFragments.Length; i++)
+ {
+ if (!String.Equals(leftFragments[i], rightFragments[i], stringComparison))
+ {
+ break;
+ }
+ }
+
+ if (i == 0)
+ {
+ return right;
+ }
+
+ var newFragments = new List();
+
+ for (int j = i; j < rightFragments.Length; j++)
+ {
+ newFragments.Add("..");
+ }
+
+ for (int j = i; j < leftFragments.Length; j++)
+ {
+ newFragments.Add(leftFragments[j]);
+ }
+
+ if (left.FullPath.EndsWith("\\") || left.FullPath.EndsWith("/"))
+ {
+ newFragments.Add(String.Empty);
+ }
+
+ if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows)
+ {
+ return new OsPath(String.Join("\\", newFragments), OsPathKind.Unknown);
+ }
+ else
+ {
+ return new OsPath(String.Join("/", newFragments), OsPathKind.Unknown);
+ }
+ }
+ }
+
+ public enum OsPathKind
+ {
+ Unknown,
+ Windows,
+ Unix
+ }
+}
diff --git a/src/NzbDrone.Common/Disk/TransferMode.cs b/src/NzbDrone.Common/Disk/TransferMode.cs
new file mode 100644
index 000000000..7b03db836
--- /dev/null
+++ b/src/NzbDrone.Common/Disk/TransferMode.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Common.Disk
+{
+ [Flags]
+ public enum TransferMode
+ {
+ None = 0,
+
+ Move = 1,
+ Copy = 2,
+ HardLink = 4,
+
+ HardLinkOrCopy = Copy | HardLink
+ }
+}
diff --git a/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs b/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs
new file mode 100644
index 000000000..328a4bdb0
--- /dev/null
+++ b/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NzbDrone.Common.Serializer;
+
+namespace NzbDrone.Common.Http
+{
+ public class JsonRpcRequestBuilder : HttpRequestBuilder
+ {
+ public String Method { get; private set; }
+ public List