commit
ebb2b12400
@ -0,0 +1,321 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MonoTorrent.BEncoding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Class representing a BEncoded Dictionary
|
||||||
|
/// </summary>
|
||||||
|
public class BEncodedDictionary : BEncodedValue, IDictionary<BEncodedString, BEncodedValue>
|
||||||
|
{
|
||||||
|
#region Member Variables
|
||||||
|
|
||||||
|
private SortedDictionary<BEncodedString, BEncodedValue> dictionary;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncodedDictionary
|
||||||
|
/// </summary>
|
||||||
|
public BEncodedDictionary()
|
||||||
|
{
|
||||||
|
this.dictionary = new SortedDictionary<BEncodedString, BEncodedValue>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Encode/Decode Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes the dictionary to a byte[]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to encode the data to</param>
|
||||||
|
/// <param name="offset">The offset to start writing the data to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override int Encode(byte[] buffer, int offset)
|
||||||
|
{
|
||||||
|
int written = 0;
|
||||||
|
|
||||||
|
//Dictionaries start with 'd'
|
||||||
|
buffer[offset] = (byte)'d';
|
||||||
|
written++;
|
||||||
|
|
||||||
|
foreach (KeyValuePair<BEncodedString, BEncodedValue> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader"></param>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the size of the dictionary in bytes using UTF8 encoding
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override int LengthInBytes()
|
||||||
|
{
|
||||||
|
int length = 0;
|
||||||
|
length += 1; // Dictionaries start with 'd'
|
||||||
|
|
||||||
|
foreach (KeyValuePair<BEncodedString, BEncodedValue> 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<BEncodedString, BEncodedValue> 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<BEncodedString, BEncodedValue> 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<BEncodedString, BEncodedValue> item)
|
||||||
|
{
|
||||||
|
this.dictionary.Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
this.dictionary.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(KeyValuePair<BEncodedString, BEncodedValue> 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<BEncodedString, BEncodedValue>[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
this.dictionary.CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get { return this.dictionary.Count; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//public int IndexOf(KeyValuePair<BEncodedString, IBEncodedValue> item)
|
||||||
|
//{
|
||||||
|
// return this.dictionary.IndexOf(item);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//public void Insert(int index, KeyValuePair<BEncodedString, IBEncodedValue> 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<BEncodedString, BEncodedValue> 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<BEncodedString, IBEncodedValue> this[int index]
|
||||||
|
//{
|
||||||
|
// get { return this.dictionary[index]; }
|
||||||
|
// set { this.dictionary[index] = value; }
|
||||||
|
//}
|
||||||
|
|
||||||
|
public ICollection<BEncodedString> Keys
|
||||||
|
{
|
||||||
|
get { return this.dictionary.Keys; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICollection<BEncodedValue> Values
|
||||||
|
{
|
||||||
|
get { return this.dictionary.Values; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<KeyValuePair<BEncodedString, BEncodedValue>> GetEnumerator()
|
||||||
|
{
|
||||||
|
return this.dictionary.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return this.dictionary.GetEnumerator();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,219 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace MonoTorrent.BEncoding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Class representing a BEncoded list
|
||||||
|
/// </summary>
|
||||||
|
public class BEncodedList : BEncodedValue, IList<BEncodedValue>
|
||||||
|
{
|
||||||
|
#region Member Variables
|
||||||
|
|
||||||
|
private List<BEncodedValue> list;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncoded List with default capacity
|
||||||
|
/// </summary>
|
||||||
|
public BEncodedList()
|
||||||
|
: this(new List<BEncodedValue>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncoded List with the supplied capacity
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="capacity">The initial capacity</param>
|
||||||
|
public BEncodedList(int capacity)
|
||||||
|
: this(new List<BEncodedValue>(capacity))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public BEncodedList(IEnumerable<BEncodedValue> list)
|
||||||
|
{
|
||||||
|
if (list == null)
|
||||||
|
throw new ArgumentNullException("list");
|
||||||
|
|
||||||
|
this.list = new List<BEncodedValue>(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BEncodedList(List<BEncodedValue> value)
|
||||||
|
{
|
||||||
|
this.list = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Encode/Decode Methods
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes the list to a byte[]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to encode the list to</param>
|
||||||
|
/// <param name="offset">The offset to start writing the data at</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a BEncodedList from the given StreamReader
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader"></param>
|
||||||
|
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
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the size of the list in bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
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<BEncodedValue> 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<BEncodedValue> GetEnumerator()
|
||||||
|
{
|
||||||
|
return this.list.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return this.GetEnumerator();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MonoTorrent.BEncoding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Class representing a BEncoded number
|
||||||
|
/// </summary>
|
||||||
|
public class BEncodedNumber : BEncodedValue, IComparable<BEncodedNumber>
|
||||||
|
{
|
||||||
|
#region Member Variables
|
||||||
|
/// <summary>
|
||||||
|
/// The value of the BEncodedNumber
|
||||||
|
/// </summary>
|
||||||
|
public long Number
|
||||||
|
{
|
||||||
|
get { return number; }
|
||||||
|
set { number = value; }
|
||||||
|
}
|
||||||
|
internal long number;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
public BEncodedNumber()
|
||||||
|
: this(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncoded number with the given value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="initialValue">The inital value of the BEncodedNumber</param>
|
||||||
|
public BEncodedNumber(long value)
|
||||||
|
{
|
||||||
|
this.number = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator BEncodedNumber(long value)
|
||||||
|
{
|
||||||
|
return new BEncodedNumber(value);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Encode/Decode Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes this number to the supplied byte[] starting at the supplied offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to write the data to</param>
|
||||||
|
/// <param name="offset">The offset to start writing the data at</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a BEncoded number from the supplied RawReader
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">RawReader containing a BEncoded Number</param>
|
||||||
|
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
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the length of the encoded string in bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
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
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
BEncodedNumber obj2 = obj as BEncodedNumber;
|
||||||
|
if (obj2 == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (this.number == obj2.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return this.number.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return (this.number.ToString());
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,220 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Text;
|
||||||
|
using MonoTorrent.Common;
|
||||||
|
using MonoTorrent.Messages;
|
||||||
|
|
||||||
|
namespace MonoTorrent.BEncoding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Class representing a BEncoded string
|
||||||
|
/// </summary>
|
||||||
|
public class BEncodedString : BEncodedValue, IComparable<BEncodedString>
|
||||||
|
{
|
||||||
|
#region Member Variables
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value of the BEncodedString
|
||||||
|
/// </summary>
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get { return Encoding.UTF8.GetString(textBytes); }
|
||||||
|
set { textBytes = Encoding.UTF8.GetBytes(value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying byte[] associated with this BEncodedString
|
||||||
|
/// </summary>
|
||||||
|
public byte[] TextBytes
|
||||||
|
{
|
||||||
|
get { return this.textBytes; }
|
||||||
|
}
|
||||||
|
private byte[] textBytes;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncodedString using UTF8 encoding
|
||||||
|
/// </summary>
|
||||||
|
public BEncodedString()
|
||||||
|
: this(new byte[0])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncodedString using UTF8 encoding
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public BEncodedString(char[] value)
|
||||||
|
: this(System.Text.Encoding.UTF8.GetBytes(value))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncodedString using UTF8 encoding
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Initial value for the string</param>
|
||||||
|
public BEncodedString(string value)
|
||||||
|
: this(System.Text.Encoding.UTF8.GetBytes(value))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new BEncodedString using UTF8 encoding
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes the BEncodedString to a byte[] using the supplied Encoding
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to encode the string to</param>
|
||||||
|
/// <param name="offset">The offset at which to save the data to</param>
|
||||||
|
/// <param name="e">The encoding to use</param>
|
||||||
|
/// <returns>The number of bytes encoded</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a BEncodedString from the supplied StreamReader
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The StreamReader containing the BEncodedString</param>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,203 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MonoTorrent.BEncoding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base interface for all BEncoded values.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BEncodedValue
|
||||||
|
{
|
||||||
|
internal abstract void DecodeInternal(RawReader reader);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes the BEncodedValue into a byte array
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Byte array containing the BEncoded Data</returns>
|
||||||
|
public byte[] Encode()
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[LengthInBytes()];
|
||||||
|
if (Encode(buffer, 0) != buffer.Length)
|
||||||
|
throw new BEncodingException("Error encoding the data");
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes the BEncodedValue into the supplied buffer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer to encode the information to</param>
|
||||||
|
/// <param name="offset">The offset in the buffer to start writing the data</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract int Encode(byte[] buffer, int offset);
|
||||||
|
|
||||||
|
public static T Clone <T> (T value)
|
||||||
|
where T : BEncodedValue
|
||||||
|
{
|
||||||
|
Check.Value (value);
|
||||||
|
return (T) BEncodedValue.Decode (value.Encode ());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for all BEncoded values
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The byte array containing the BEncoded data</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode BEncoded data in the given byte array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The byte array containing the BEncoded data</param>
|
||||||
|
/// <param name="offset">The offset at which the data starts at</param>
|
||||||
|
/// <param name="length">The number of bytes to be decoded</param>
|
||||||
|
/// <returns>BEncodedValue containing the data that was in the byte[]</returns>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode BEncoded data in the given stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">The stream containing the BEncoded data</param>
|
||||||
|
/// <returns>BEncodedValue containing the data that was in the stream</returns>
|
||||||
|
public static BEncodedValue Decode(Stream stream)
|
||||||
|
{
|
||||||
|
if (stream == null)
|
||||||
|
throw new ArgumentNullException("stream");
|
||||||
|
|
||||||
|
return Decode(new RawReader(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode BEncoded data in the given RawReader
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The RawReader containing the BEncoded data</param>
|
||||||
|
/// <returns>BEncodedValue containing the data that was in the stream</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for all BEncoded values
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The byte array containing the BEncoded data</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static T Decode<T>(byte[] data) where T : BEncodedValue
|
||||||
|
{
|
||||||
|
return (T)BEncodedValue.Decode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode BEncoded data in the given byte array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The byte array containing the BEncoded data</param>
|
||||||
|
/// <param name="offset">The offset at which the data starts at</param>
|
||||||
|
/// <param name="length">The number of bytes to be decoded</param>
|
||||||
|
/// <returns>BEncodedValue containing the data that was in the byte[]</returns>
|
||||||
|
public static T Decode<T>(byte[] buffer, int offset, int length) where T : BEncodedValue
|
||||||
|
{
|
||||||
|
return BEncodedValue.Decode<T>(buffer, offset, length, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Decode<T>(byte[] buffer, int offset, int length, bool strictDecoding) where T : BEncodedValue
|
||||||
|
{
|
||||||
|
return (T)BEncodedValue.Decode(buffer, offset, length, strictDecoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode BEncoded data in the given stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">The stream containing the BEncoded data</param>
|
||||||
|
/// <returns>BEncodedValue containing the data that was in the stream</returns>
|
||||||
|
public static T Decode<T>(Stream stream) where T : BEncodedValue
|
||||||
|
{
|
||||||
|
return (T)BEncodedValue.Decode(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static T Decode<T>(RawReader reader) where T : BEncodedValue
|
||||||
|
{
|
||||||
|
return (T)BEncodedValue.Decode(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the size of the byte[] needed to encode this BEncodedValue
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract int LengthInBytes();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,420 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MonoTorrent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class is for represting the Peer's bitfield
|
||||||
|
/// </summary>
|
||||||
|
public class BitField : ICloneable, IEnumerable<bool>
|
||||||
|
{
|
||||||
|
#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<bool> 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
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace MonoTorrent
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
DoNotDownload = 0,
|
||||||
|
Lowest = 1,
|
||||||
|
Low = 2,
|
||||||
|
Normal = 4,
|
||||||
|
High = 8,
|
||||||
|
Highest = 16,
|
||||||
|
Immediate = 32
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace MonoTorrent
|
||||||
|
{
|
||||||
|
public static class HashAlgoFactory
|
||||||
|
{
|
||||||
|
static Dictionary<Type, Type> algos = new Dictionary<Type, Type>();
|
||||||
|
|
||||||
|
static HashAlgoFactory()
|
||||||
|
{
|
||||||
|
Register<MD5, MD5CryptoServiceProvider>();
|
||||||
|
Register<SHA1, SHA1CryptoServiceProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Register<T, U>()
|
||||||
|
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<T>()
|
||||||
|
where T : HashAlgorithm
|
||||||
|
{
|
||||||
|
if (algos.ContainsKey(typeof(T)))
|
||||||
|
return (T)Activator.CreateInstance(algos[typeof(T)]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MonoTorrent
|
||||||
|
{
|
||||||
|
public class Hashes
|
||||||
|
{
|
||||||
|
#region Constants
|
||||||
|
/// <summary>
|
||||||
|
/// Hash code length (in bytes)
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly int HashCodeLength = 20;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
|
private int count;
|
||||||
|
private byte[] hashData;
|
||||||
|
|
||||||
|
#endregion Private Fields
|
||||||
|
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of Hashes (equivalent to number of Pieces)
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine whether a calculated hash is equal to our stored hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">Hash code to check</param>
|
||||||
|
/// <param name="hashIndex">Index of hash/piece to verify against</param>
|
||||||
|
/// <returns>true iff hash is equal to our stored hash, false otherwise</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the hash for a specific piece
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hashIndex">Piece/hash index to return</param>
|
||||||
|
/// <returns>byte[] (length HashCodeLength) containing hashdata</returns>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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 <InfoHash>
|
||||||
|
{
|
||||||
|
static Dictionary<char, byte> base32DecodeTable;
|
||||||
|
|
||||||
|
static InfoHash()
|
||||||
|
{
|
||||||
|
base32DecodeTable = new Dictionary<char, byte>();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<string> Webseeds {
|
||||||
|
get; private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MagnetLink (string url)
|
||||||
|
{
|
||||||
|
Check.Url (url);
|
||||||
|
AnnounceUrls = new RawTrackerTier ();
|
||||||
|
Webseeds = new List<string> ();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using MonoTorrent.BEncoding;
|
||||||
|
|
||||||
|
namespace MonoTorrent
|
||||||
|
{
|
||||||
|
public class RawTrackerTier : IList<string>
|
||||||
|
{
|
||||||
|
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<string> 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<string> GetEnumerator ()
|
||||||
|
{
|
||||||
|
foreach (BEncodedString v in Tier)
|
||||||
|
yield return v.Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator ()
|
||||||
|
{
|
||||||
|
return GetEnumerator ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using MonoTorrent.BEncoding;
|
||||||
|
|
||||||
|
namespace MonoTorrent
|
||||||
|
{
|
||||||
|
public class RawTrackerTiers : IList<RawTrackerTier>
|
||||||
|
{
|
||||||
|
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<RawTrackerTier> 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<RawTrackerTier> GetEnumerator ()
|
||||||
|
{
|
||||||
|
foreach (var v in Tiers)
|
||||||
|
yield return new RawTrackerTier ((BEncodedList) v);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator ()
|
||||||
|
{
|
||||||
|
return GetEnumerator ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>(T target);
|
||||||
|
|
||||||
|
public static class Toolbox
|
||||||
|
{
|
||||||
|
private static Random r = new Random();
|
||||||
|
public static int Count<T>(IEnumerable<T> enumerable, Predicate<T> predicate)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
foreach (T t in enumerable)
|
||||||
|
if (predicate(t))
|
||||||
|
count++;
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long Accumulate<T>(IEnumerable<T> enumerable, Operation<T> action)
|
||||||
|
{
|
||||||
|
long count = 0;
|
||||||
|
|
||||||
|
foreach (T t in enumerable)
|
||||||
|
count += action(t);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RaiseAsyncEvent<T>(EventHandler<T> e, object o, T args)
|
||||||
|
where T : EventArgs
|
||||||
|
{
|
||||||
|
if (e == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ThreadPool.QueueUserWorkItem(delegate {
|
||||||
|
if (e != null)
|
||||||
|
e(o, args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Randomizes the contents of the array
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="array"></param>
|
||||||
|
public static void Randomize<T>(List<T> array)
|
||||||
|
{
|
||||||
|
List<T> clone = new List<T>(array);
|
||||||
|
array.Clear();
|
||||||
|
|
||||||
|
while (clone.Count > 0)
|
||||||
|
{
|
||||||
|
int index = r.Next(0, clone.Count);
|
||||||
|
array.Add(clone[index]);
|
||||||
|
clone.RemoveAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switches the positions of two elements in an array
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="array"></param>
|
||||||
|
/// <param name="first"></param>
|
||||||
|
/// <param name="second"></param>
|
||||||
|
public static void Switch<T>(IList<T> array, int first, int second)
|
||||||
|
{
|
||||||
|
T obj = array[first];
|
||||||
|
array[first] = array[second];
|
||||||
|
array[second] = obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if the contents of two byte arrays are equal
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array1">The first array</param>
|
||||||
|
/// <param name="array2">The second array</param>
|
||||||
|
/// <returns>True if the arrays are equal, false if they aren't</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if the contents of two byte arrays are equal
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array1">The first array</param>
|
||||||
|
/// <param name="array2">The second array</param>
|
||||||
|
/// <param name="offset1">The starting index for the first array</param>
|
||||||
|
/// <param name="offset2">The starting index for the second array</param>
|
||||||
|
/// <param name="count">The number of bytes to check</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The "Torrent" class for both Tracker and Client should inherit from this
|
||||||
|
/// as it contains the fields that are common to both.
|
||||||
|
/// </summary>
|
||||||
|
public class Torrent : IEquatable<Torrent>
|
||||||
|
{
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
|
private BEncodedDictionary originalDictionary;
|
||||||
|
private BEncodedValue azureusProperties;
|
||||||
|
private IList<RawTrackerTier> 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<string> getRightHttpSeeds;
|
||||||
|
private byte[] metadata;
|
||||||
|
|
||||||
|
#endregion Private Fields
|
||||||
|
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
internal byte[] Metadata
|
||||||
|
{
|
||||||
|
get { return this.metadata; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The announce URLs contained within the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public IList<RawTrackerTier> AnnounceUrls
|
||||||
|
{
|
||||||
|
get { return this.announceUrls; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
public BEncodedValue AzureusProperties
|
||||||
|
{
|
||||||
|
get { return this.azureusProperties; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The comment contained within the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public string Comment
|
||||||
|
{
|
||||||
|
get { return this.comment; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The optional string showing who/what created the .torrent
|
||||||
|
/// </summary>
|
||||||
|
public string CreatedBy
|
||||||
|
{
|
||||||
|
get { return this.createdBy; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The creation date of the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreationDate
|
||||||
|
{
|
||||||
|
get { return this.creationDate; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The optional ED2K hash contained within the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public byte[] ED2K
|
||||||
|
{
|
||||||
|
get { return this.ed2k; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The encoding used by the client that created the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public string Encoding
|
||||||
|
{
|
||||||
|
get { return this.encoding; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of files contained within the .torrent which are available for download
|
||||||
|
/// </summary>
|
||||||
|
public TorrentFile[] Files
|
||||||
|
{
|
||||||
|
get { return this.torrentFiles; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is the infohash that is generated by putting the "Info" section of a .torrent
|
||||||
|
/// through a ManagedSHA1 hasher.
|
||||||
|
/// </summary>
|
||||||
|
public InfoHash InfoHash
|
||||||
|
{
|
||||||
|
get { return this.infoHash; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows whether DHT is allowed or not. If it is a private torrent, no peer
|
||||||
|
/// sharing should be allowed.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPrivate
|
||||||
|
{
|
||||||
|
get { return this.isPrivate; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get { return this.name; }
|
||||||
|
private set { this.name = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FIXME: No idea what this is.
|
||||||
|
/// </summary>
|
||||||
|
public BEncodedList Nodes
|
||||||
|
{
|
||||||
|
get { return this.nodes; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The length of each piece in bytes.
|
||||||
|
/// </summary>
|
||||||
|
public int PieceLength
|
||||||
|
{
|
||||||
|
get { return this.pieceLength; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is the array of hashes contained within the torrent.
|
||||||
|
/// </summary>
|
||||||
|
public Hashes Pieces
|
||||||
|
{
|
||||||
|
get { return this.pieces; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the Publisher
|
||||||
|
/// </summary>
|
||||||
|
public string Publisher
|
||||||
|
{
|
||||||
|
get { return this.publisher; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Url of the publisher of either the content or the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public string PublisherUrl
|
||||||
|
{
|
||||||
|
get { return this.publisherUrl; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The optional SHA1 hash contained within the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public byte[] SHA1
|
||||||
|
{
|
||||||
|
get { return this.sha1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total size of all the files that have to be downloaded.
|
||||||
|
/// </summary>
|
||||||
|
public long Size
|
||||||
|
{
|
||||||
|
get { return this.size; }
|
||||||
|
private set { this.size = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The source of the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
public string Source
|
||||||
|
{
|
||||||
|
get { return this.source; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is the path at which the .torrent file is located
|
||||||
|
/// </summary>
|
||||||
|
public string TorrentPath
|
||||||
|
{
|
||||||
|
get { return this.torrentPath; }
|
||||||
|
internal set { this.torrentPath = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is the http-based seeding (getright protocole)
|
||||||
|
/// </summary>
|
||||||
|
public List<string> 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<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method is called internally to read out the hashes from the info section of the
|
||||||
|
/// .torrent file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The byte[]containing the hashes from the .torrent file</param>
|
||||||
|
private void LoadHashPieces(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length % 20 != 0)
|
||||||
|
throw new TorrentException("Invalid infohash detected");
|
||||||
|
|
||||||
|
this.pieces = new Hashes(data, data.Length / 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method is called internally to load in all the files found within the "Files" section
|
||||||
|
/// of the .torrents infohash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="list">The list containing the files available to download</param>
|
||||||
|
private void LoadTorrentFiles(BEncodedList list)
|
||||||
|
{
|
||||||
|
List<TorrentFile> files = new List<TorrentFile>();
|
||||||
|
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<BEncodedString, BEncodedValue> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method is called internally to load the information found within the "Info" section
|
||||||
|
/// of the .torrent file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dictionary">The dictionary representing the Info section of the .torrent file</param>
|
||||||
|
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<BEncodedString, BEncodedValue> 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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method loads a .torrent file from the specified path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to load the .torrent file from</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a torrent from a byte[] containing the bencoded data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The byte[] containing the data</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Torrent Load(byte[] data)
|
||||||
|
{
|
||||||
|
Check.Data(data);
|
||||||
|
|
||||||
|
using (MemoryStream s = new MemoryStream(data))
|
||||||
|
return Load(s, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a .torrent from the supplied stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">The stream containing the data to load</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Torrent Load(Stream stream)
|
||||||
|
{
|
||||||
|
Check.Stream(stream);
|
||||||
|
|
||||||
|
if (stream == null)
|
||||||
|
throw new ArgumentNullException("stream");
|
||||||
|
|
||||||
|
return Torrent.Load(stream, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a .torrent file from the specified URL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The URL to download the .torrent from</param>
|
||||||
|
/// <param name="location">The path to download the .torrent to before it gets loaded</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a .torrent from the specificed path. A return value indicates
|
||||||
|
/// whether the operation was successful.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to load the .torrent file from</param>
|
||||||
|
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
|
||||||
|
/// <returns>True if successful</returns>
|
||||||
|
public static bool TryLoad(string path, out Torrent torrent)
|
||||||
|
{
|
||||||
|
Check.Path(path);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
torrent = Torrent.Load(path);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
torrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrent != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a .torrent from the specified byte[]. A return value indicates
|
||||||
|
/// whether the operation was successful.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The byte[] to load the .torrent from</param>
|
||||||
|
/// <param name="torrent">If loading was successful, it contains the Torrent</param>
|
||||||
|
/// <returns>True if successful</returns>
|
||||||
|
public static bool TryLoad(byte[] data, out Torrent torrent)
|
||||||
|
{
|
||||||
|
Check.Data(data);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
torrent = Torrent.Load(data);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
torrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrent != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a .torrent from the supplied stream. A return value indicates
|
||||||
|
/// whether the operation was successful.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">The stream containing the data to load</param>
|
||||||
|
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
|
||||||
|
/// <returns>True if successful</returns>
|
||||||
|
public static bool TryLoad(Stream stream, out Torrent torrent)
|
||||||
|
{
|
||||||
|
Check.Stream(stream);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
torrent = Torrent.Load(stream);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
torrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrent != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a .torrent file from the specified URL. A return value indicates
|
||||||
|
/// whether the operation was successful.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The URL to download the .torrent from</param>
|
||||||
|
/// <param name="location">The path to download the .torrent to before it gets loaded</param>
|
||||||
|
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
|
||||||
|
/// <returns>True if successful</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called from either Load(stream) or Load(string).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream"></param>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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<BEncodedString, BEncodedValue> 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<SHA1>())
|
||||||
|
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<string> tier = new List<string>(bencodedTier.Count);
|
||||||
|
|
||||||
|
for (int k = 0; k < bencodedTier.Count; k++)
|
||||||
|
tier.Add(bencodedTier[k].ToString());
|
||||||
|
|
||||||
|
Toolbox.Randomize<string>(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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,205 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MonoTorrent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
public class TorrentFile : IEquatable<TorrentFile>
|
||||||
|
{
|
||||||
|
#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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of pieces which have been successfully downloaded which are from this file
|
||||||
|
/// </summary>
|
||||||
|
public BitField BitField
|
||||||
|
{
|
||||||
|
get { return this.bitfield; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public long BytesDownloaded
|
||||||
|
{
|
||||||
|
get { return (long)(this.BitField.PercentComplete * this.Length / 100.0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ED2K hash of the file
|
||||||
|
/// </summary>
|
||||||
|
public byte[] ED2K
|
||||||
|
{
|
||||||
|
get { return this.ed2k; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the last piece of this file
|
||||||
|
/// </summary>
|
||||||
|
public int EndPieceIndex
|
||||||
|
{
|
||||||
|
get { return this.endPiece; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FullPath
|
||||||
|
{
|
||||||
|
get { return this.fullPath; }
|
||||||
|
internal set { this.fullPath = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The length of the file in bytes
|
||||||
|
/// </summary>
|
||||||
|
public long Length
|
||||||
|
{
|
||||||
|
get { return this.length; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The MD5 hash of the file
|
||||||
|
/// </summary>
|
||||||
|
public byte[] MD5
|
||||||
|
{
|
||||||
|
get { return this.md5; }
|
||||||
|
internal set { this.md5 = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
public string Path
|
||||||
|
{
|
||||||
|
get { return this.path; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The priority of this torrent file
|
||||||
|
/// </summary>
|
||||||
|
public Priority Priority
|
||||||
|
{
|
||||||
|
get { return this.priority; }
|
||||||
|
set { this.priority = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The SHA1 hash of the file
|
||||||
|
/// </summary>
|
||||||
|
public byte[] SHA1
|
||||||
|
{
|
||||||
|
get { return this.sha1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the first piece of this file
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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 <byte> ();
|
||||||
|
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<byte> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Linq;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Download.Clients;
|
||||||
|
using NzbDrone.Core.Download.Clients.TorrentBlackhole;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TorrentBlackholeFixture : DownloadClientFixtureBase<TorrentBlackhole>
|
||||||
|
{
|
||||||
|
protected String _completedDownloadFolder;
|
||||||
|
protected String _blackholeFolder;
|
||||||
|
protected String _filePath;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_completedDownloadFolder = @"c:\blackhole\completed".AsOsAgnostic();
|
||||||
|
_blackholeFolder = @"c:\blackhole\torrent".AsOsAgnostic();
|
||||||
|
_filePath = (@"c:\blackhole\torrent\" + _title + ".torrent").AsOsAgnostic();
|
||||||
|
|
||||||
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
|
Subject.Definition.Settings = new TorrentBlackholeSettings
|
||||||
|
{
|
||||||
|
TorrentFolder = _blackholeFolder,
|
||||||
|
WatchFolder = _completedDownloadFolder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenFailedDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Throws(new WebException());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenCompletedItem()
|
||||||
|
{
|
||||||
|
var targetDir = Path.Combine(_completedDownloadFolder, _title);
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(c => c.GetDirectories(_completedDownloadFolder))
|
||||||
|
.Returns(new[] { targetDir });
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(c => c.GetFiles(targetDir, SearchOption.AllDirectories))
|
||||||
|
.Returns(new[] { Path.Combine(_completedDownloadFolder, "somefile.mkv") });
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(c => c.GetFileSize(It.IsAny<String>()))
|
||||||
|
.Returns(1000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void completed_download_should_have_required_properties()
|
||||||
|
{
|
||||||
|
GivenCompletedItem();
|
||||||
|
|
||||||
|
var result = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
VerifyCompleted(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_category()
|
||||||
|
{
|
||||||
|
GivenCompletedItem();
|
||||||
|
|
||||||
|
var result = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
// We must have a category or CDH won't pick it up.
|
||||||
|
result.Category.Should().NotBeNullOrWhiteSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_download_file_if_it_doesnt_exist()
|
||||||
|
{
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_downloadUrl, _filePath), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_replace_illegal_characters_in_title()
|
||||||
|
{
|
||||||
|
var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]";
|
||||||
|
var expectedFilename = Path.Combine(_blackholeFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV]" + Path.GetExtension(_filePath));
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
remoteEpisode.Release.Title = illegalTitle;
|
||||||
|
|
||||||
|
Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), expectedFilename), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_considered_locked_files_queued()
|
||||||
|
{
|
||||||
|
GivenCompletedItem();
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(c => c.IsFileLocked(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var items = Subject.GetItems().ToList();
|
||||||
|
|
||||||
|
items.Count.Should().Be(1);
|
||||||
|
items.First().Status.Should().Be(DownloadItemStatus.Downloading);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_status_with_outputdirs()
|
||||||
|
{
|
||||||
|
var result = Subject.GetStatus();
|
||||||
|
|
||||||
|
result.IsLocalhost.Should().BeTrue();
|
||||||
|
result.OutputRootFolders.Should().NotBeNull();
|
||||||
|
result.OutputRootFolders.First().Should().Be(_completedDownloadFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,310 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Download.Clients.Deluge;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class DelugeFixture : DownloadClientFixtureBase<Deluge>
|
||||||
|
{
|
||||||
|
protected DelugeTorrent _queued;
|
||||||
|
protected DelugeTorrent _downloading;
|
||||||
|
protected DelugeTorrent _failed;
|
||||||
|
protected DelugeTorrent _completed;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
|
Subject.Definition.Settings = new DelugeSettings()
|
||||||
|
{
|
||||||
|
TvCategory = null
|
||||||
|
};
|
||||||
|
|
||||||
|
_queued = new DelugeTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
IsFinished = false,
|
||||||
|
State = DelugeTorrentStatus.Queued,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
BytesDownloaded = 0,
|
||||||
|
Progress = 0.0,
|
||||||
|
DownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_downloading = new DelugeTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
IsFinished = false,
|
||||||
|
State = DelugeTorrentStatus.Downloading,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
BytesDownloaded = 100,
|
||||||
|
Progress = 10.0,
|
||||||
|
DownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_failed = new DelugeTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
IsFinished = false,
|
||||||
|
State = DelugeTorrentStatus.Error,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
BytesDownloaded = 100,
|
||||||
|
Progress = 10.0,
|
||||||
|
Message = "Error",
|
||||||
|
DownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_completed = new DelugeTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
IsFinished = true,
|
||||||
|
State = DelugeTorrentStatus.Paused,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
BytesDownloaded = 1000,
|
||||||
|
Progress = 100.0,
|
||||||
|
DownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<ITorrentFileInfoReader>()
|
||||||
|
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
|
||||||
|
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenFailedDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IDelugeProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromMagnet(It.IsAny<String>(), It.IsAny<DelugeSettings>()))
|
||||||
|
.Throws<InvalidOperationException>();
|
||||||
|
|
||||||
|
Mocker.GetMock<IDelugeProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromFile(It.IsAny<String>(), It.IsAny<Byte[]>(), It.IsAny<DelugeSettings>()))
|
||||||
|
.Throws<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenSuccessfulDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[1000]));
|
||||||
|
|
||||||
|
Mocker.GetMock<IDelugeProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromMagnet(It.IsAny<String>(), It.IsAny<DelugeSettings>()))
|
||||||
|
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951".ToLower())
|
||||||
|
.Callback(PrepareClientToReturnQueuedItem);
|
||||||
|
|
||||||
|
Mocker.GetMock<IDelugeProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromFile(It.IsAny<String>(), It.IsAny<Byte[]>(), It.IsAny<DelugeSettings>()))
|
||||||
|
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951".ToLower())
|
||||||
|
.Callback(PrepareClientToReturnQueuedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenTorrents(List<DelugeTorrent> torrents)
|
||||||
|
{
|
||||||
|
if (torrents == null)
|
||||||
|
{
|
||||||
|
torrents = new List<DelugeTorrent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mocker.GetMock<IDelugeProxy>()
|
||||||
|
.Setup(s => s.GetTorrents(It.IsAny<DelugeSettings>()))
|
||||||
|
.Returns(torrents.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnQueuedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<DelugeTorrent>
|
||||||
|
{
|
||||||
|
_queued
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnDownloadingItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<DelugeTorrent>
|
||||||
|
{
|
||||||
|
_downloading
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnFailedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<DelugeTorrent>
|
||||||
|
{
|
||||||
|
_failed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnCompletedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<DelugeTorrent>
|
||||||
|
{
|
||||||
|
_completed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void queued_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnQueuedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyQueued(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void downloading_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnDownloadingItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyDownloading(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void failed_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnFailedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyFailed(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void completed_download_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyCompleted(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_return_unique_id()
|
||||||
|
{
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
|
||||||
|
public void Download_should_get_hash_from_magnet_url(String magnetUrl, String expectedHash)
|
||||||
|
{
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
remoteEpisode.Release.DownloadUrl = magnetUrl;
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().Be(expectedHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Paused)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
|
||||||
|
public void GetItems_should_return_queued_item_as_downloadItemStatus(String apiStatus, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_queued.State = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnQueuedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Paused)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
|
||||||
|
public void GetItems_should_return_downloading_item_as_downloadItemStatus(String apiStatus, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_downloading.State = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnDownloadingItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Completed, true)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading, true)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
|
||||||
|
[TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Completed, true)]
|
||||||
|
public void GetItems_should_return_completed_item_as_downloadItemStatus(String apiStatus, DownloadItemStatus expectedItemStatus, Boolean expectedReadOnly)
|
||||||
|
{
|
||||||
|
_completed.State = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
item.IsReadOnly.Should().Be(expectedReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_check_share_ratio_for_readonly()
|
||||||
|
{
|
||||||
|
_completed.State = DelugeTorrentStatus.Paused;
|
||||||
|
_completed.IsAutoManaged = true;
|
||||||
|
_completed.StopAtRatio = true;
|
||||||
|
_completed.StopRatio = 1.0;
|
||||||
|
_completed.Ratio = 1.01;
|
||||||
|
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(DownloadItemStatus.Completed);
|
||||||
|
item.IsReadOnly.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_status_with_outputdirs()
|
||||||
|
{
|
||||||
|
var configItems = new Dictionary<String, Object>();
|
||||||
|
|
||||||
|
configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic());
|
||||||
|
configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||||
|
configItems.Add("move_completed", true);
|
||||||
|
|
||||||
|
Mocker.GetMock<IDelugeProxy>()
|
||||||
|
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
|
||||||
|
.Returns(configItems);
|
||||||
|
|
||||||
|
var result = Subject.GetStatus();
|
||||||
|
|
||||||
|
result.IsLocalhost.Should().BeTrue();
|
||||||
|
result.OutputRootFolders.Should().NotBeNull();
|
||||||
|
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,363 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Download.Clients.Transmission;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TransmissionFixture : DownloadClientFixtureBase<Transmission>
|
||||||
|
{
|
||||||
|
protected TransmissionSettings _settings;
|
||||||
|
protected TransmissionTorrent _queued;
|
||||||
|
protected TransmissionTorrent _downloading;
|
||||||
|
protected TransmissionTorrent _failed;
|
||||||
|
protected TransmissionTorrent _completed;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_settings = new TransmissionSettings
|
||||||
|
{
|
||||||
|
Host = "127.0.0.1",
|
||||||
|
Port = 2222,
|
||||||
|
Username = "admin",
|
||||||
|
Password = "pass"
|
||||||
|
};
|
||||||
|
|
||||||
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
|
Subject.Definition.Settings = _settings;
|
||||||
|
|
||||||
|
_queued = new TransmissionTorrent
|
||||||
|
{
|
||||||
|
HashString = "HASH",
|
||||||
|
IsFinished = false,
|
||||||
|
Status = TransmissionTorrentStatus.Queued,
|
||||||
|
Name = _title,
|
||||||
|
TotalSize = 1000,
|
||||||
|
LeftUntilDone = 1000,
|
||||||
|
DownloadDir = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_downloading = new TransmissionTorrent
|
||||||
|
{
|
||||||
|
HashString = "HASH",
|
||||||
|
IsFinished = false,
|
||||||
|
Status = TransmissionTorrentStatus.Downloading,
|
||||||
|
Name = _title,
|
||||||
|
TotalSize = 1000,
|
||||||
|
LeftUntilDone = 100,
|
||||||
|
DownloadDir = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_failed = new TransmissionTorrent
|
||||||
|
{
|
||||||
|
HashString = "HASH",
|
||||||
|
IsFinished = false,
|
||||||
|
Status = TransmissionTorrentStatus.Stopped,
|
||||||
|
Name = _title,
|
||||||
|
TotalSize = 1000,
|
||||||
|
LeftUntilDone = 100,
|
||||||
|
ErrorString = "Error",
|
||||||
|
DownloadDir = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_completed = new TransmissionTorrent
|
||||||
|
{
|
||||||
|
HashString = "HASH",
|
||||||
|
IsFinished = true,
|
||||||
|
Status = TransmissionTorrentStatus.Stopped,
|
||||||
|
Name = _title,
|
||||||
|
TotalSize = 1000,
|
||||||
|
LeftUntilDone = 0,
|
||||||
|
DownloadDir = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<ITorrentFileInfoReader>()
|
||||||
|
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
|
||||||
|
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
||||||
|
|
||||||
|
var configItems = new Dictionary<String, Object>();
|
||||||
|
|
||||||
|
configItems.Add("download-dir", @"C:/Downloads/Finished/transmission");
|
||||||
|
configItems.Add("incomplete-dir", null);
|
||||||
|
configItems.Add("incomplete-dir-enabled", false);
|
||||||
|
|
||||||
|
Mocker.GetMock<ITransmissionProxy>()
|
||||||
|
.Setup(v => v.GetConfig(It.IsAny<TransmissionSettings>()))
|
||||||
|
.Returns(configItems);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenTvCategory()
|
||||||
|
{
|
||||||
|
_settings.TvCategory = "nzbdrone";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenFailedDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<ITransmissionProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromUrl(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<TransmissionSettings>()))
|
||||||
|
.Throws<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenSuccessfulDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[1000]));
|
||||||
|
|
||||||
|
Mocker.GetMock<ITransmissionProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromUrl(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<TransmissionSettings>()))
|
||||||
|
.Callback(PrepareClientToReturnQueuedItem);
|
||||||
|
|
||||||
|
Mocker.GetMock<ITransmissionProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromData(It.IsAny<Byte[]>(), It.IsAny<String>(), It.IsAny<TransmissionSettings>()))
|
||||||
|
.Callback(PrepareClientToReturnQueuedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenTorrents(List<TransmissionTorrent> torrents)
|
||||||
|
{
|
||||||
|
if (torrents == null)
|
||||||
|
{
|
||||||
|
torrents = new List<TransmissionTorrent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mocker.GetMock<ITransmissionProxy>()
|
||||||
|
.Setup(s => s.GetTorrents(It.IsAny<TransmissionSettings>()))
|
||||||
|
.Returns(torrents);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnQueuedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<TransmissionTorrent>
|
||||||
|
{
|
||||||
|
_queued
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnDownloadingItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<TransmissionTorrent>
|
||||||
|
{
|
||||||
|
_downloading
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnFailedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<TransmissionTorrent>
|
||||||
|
{
|
||||||
|
_failed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnCompletedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<TransmissionTorrent>
|
||||||
|
{
|
||||||
|
_completed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void queued_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnQueuedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyQueued(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void downloading_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnDownloadingItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyDownloading(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void failed_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnFailedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyFailed(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void completed_download_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyCompleted(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_return_unique_id()
|
||||||
|
{
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_with_category_should_force_directory()
|
||||||
|
{
|
||||||
|
GivenTvCategory();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().NotBeNullOrEmpty();
|
||||||
|
|
||||||
|
Mocker.GetMock<ITransmissionProxy>()
|
||||||
|
.Verify(v => v.AddTorrentFromData(It.IsAny<Byte[]>(), @"C:/Downloads/Finished/transmission/.nzbdrone", It.IsAny<TransmissionSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
|
||||||
|
public void Download_should_get_hash_from_magnet_url(String magnetUrl, String expectedHash)
|
||||||
|
{
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
remoteEpisode.Release.DownloadUrl = magnetUrl;
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().Be(expectedHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
|
||||||
|
public void GetItems_should_return_queued_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_queued.Status = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnQueuedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
|
||||||
|
public void GetItems_should_return_downloading_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_downloading.Status = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnDownloadingItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, true)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, true)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, true)]
|
||||||
|
[TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, true)]
|
||||||
|
public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, Boolean expectedReadOnly)
|
||||||
|
{
|
||||||
|
_completed.Status = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
item.IsReadOnly.Should().Be(expectedReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_status_with_outputdirs()
|
||||||
|
{
|
||||||
|
var result = Subject.GetStatus();
|
||||||
|
|
||||||
|
result.IsLocalhost.Should().BeTrue();
|
||||||
|
result.OutputRootFolders.Should().NotBeNull();
|
||||||
|
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\transmission");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_exclude_items_not_in_category()
|
||||||
|
{
|
||||||
|
GivenTvCategory();
|
||||||
|
|
||||||
|
_downloading.DownloadDir = @"C:/Downloads/Finished/transmission/.nzbdrone";
|
||||||
|
|
||||||
|
GivenTorrents(new List<TransmissionTorrent>
|
||||||
|
{
|
||||||
|
_downloading,
|
||||||
|
_queued
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = Subject.GetItems().ToList();
|
||||||
|
|
||||||
|
items.Count.Should().Be(1);
|
||||||
|
items.First().Status.Should().Be(DownloadItemStatus.Downloading);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_fix_forward_slashes()
|
||||||
|
{
|
||||||
|
WindowsOnly();
|
||||||
|
|
||||||
|
_downloading.DownloadDir = @"C:/Downloads/Finished/transmission";
|
||||||
|
|
||||||
|
GivenTorrents(new List<TransmissionTorrent>
|
||||||
|
{
|
||||||
|
_downloading
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = Subject.GetItems().ToList();
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
items.First().OutputPath.Should().Be(@"C:\Downloads\Finished\transmission\" + _title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("2.84 ()")]
|
||||||
|
[TestCase("2.84+ ()")]
|
||||||
|
[TestCase("2.84 (other info)")]
|
||||||
|
[TestCase("2.84 (2.84)")]
|
||||||
|
public void should_version_should_only_check_version_number(String version)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<ITransmissionProxy>()
|
||||||
|
.Setup(s => s.GetVersion(It.IsAny<TransmissionSettings>()))
|
||||||
|
.Returns(version);
|
||||||
|
|
||||||
|
Subject.Test();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,340 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Download.Clients.UTorrent;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class UTorrentFixture : DownloadClientFixtureBase<UTorrent>
|
||||||
|
{
|
||||||
|
protected UTorrentTorrent _queued;
|
||||||
|
protected UTorrentTorrent _downloading;
|
||||||
|
protected UTorrentTorrent _failed;
|
||||||
|
protected UTorrentTorrent _completed;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
|
Subject.Definition.Settings = new UTorrentSettings
|
||||||
|
{
|
||||||
|
Host = "127.0.0.1",
|
||||||
|
Port = 2222,
|
||||||
|
Username = "admin",
|
||||||
|
Password = "pass",
|
||||||
|
TvCategory = "tv"
|
||||||
|
};
|
||||||
|
|
||||||
|
_queued = new UTorrentTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Status = UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Loaded,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Remaining = 1000,
|
||||||
|
Progress = 0,
|
||||||
|
Label = "tv",
|
||||||
|
DownloadUrl = _downloadUrl,
|
||||||
|
RootDownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_downloading = new UTorrentTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Status = UTorrentTorrentStatus.Started | UTorrentTorrentStatus.Loaded,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Remaining = 100,
|
||||||
|
Progress = 0.9,
|
||||||
|
Label = "tv",
|
||||||
|
DownloadUrl = _downloadUrl,
|
||||||
|
RootDownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_failed = new UTorrentTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Status = UTorrentTorrentStatus.Error,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Remaining = 100,
|
||||||
|
Progress = 0.9,
|
||||||
|
Label = "tv",
|
||||||
|
DownloadUrl = _downloadUrl,
|
||||||
|
RootDownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
_completed = new UTorrentTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Status = UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Loaded,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Remaining = 0,
|
||||||
|
Progress = 1.0,
|
||||||
|
Label = "tv",
|
||||||
|
DownloadUrl = _downloadUrl,
|
||||||
|
RootDownloadPath = "somepath"
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<ITorrentFileInfoReader>()
|
||||||
|
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
|
||||||
|
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenRedirectToMagnet()
|
||||||
|
{
|
||||||
|
var httpHeader = new HttpHeader();
|
||||||
|
httpHeader["Location"] = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp";
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new Byte[0], System.Net.HttpStatusCode.SeeOther));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenFailedDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IUTorrentProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<UTorrentSettings>()))
|
||||||
|
.Throws<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenSuccessfulDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IUTorrentProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromUrl(It.IsAny<String>(), It.IsAny<UTorrentSettings>()))
|
||||||
|
.Callback(() =>
|
||||||
|
{
|
||||||
|
PrepareClientToReturnQueuedItem();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenTorrents(List<UTorrentTorrent> torrents)
|
||||||
|
{
|
||||||
|
if (torrents == null)
|
||||||
|
{
|
||||||
|
torrents = new List<UTorrentTorrent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mocker.GetMock<IUTorrentProxy>()
|
||||||
|
.Setup(s => s.GetTorrents(It.IsAny<UTorrentSettings>()))
|
||||||
|
.Returns(torrents);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnQueuedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<UTorrentTorrent>
|
||||||
|
{
|
||||||
|
_queued
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnDownloadingItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<UTorrentTorrent>
|
||||||
|
{
|
||||||
|
_downloading
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnFailedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<UTorrentTorrent>
|
||||||
|
{
|
||||||
|
_failed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PrepareClientToReturnCompletedItem()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<UTorrentTorrent>
|
||||||
|
{
|
||||||
|
_completed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void queued_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnQueuedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyQueued(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void downloading_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnDownloadingItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyDownloading(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void failed_item_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnFailedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyFailed(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void completed_download_should_have_required_properties()
|
||||||
|
{
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
VerifyCompleted(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_return_unique_id()
|
||||||
|
{
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_ignore_downloads_from_other_categories()
|
||||||
|
{
|
||||||
|
_completed.Label = "myowncat";
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
|
||||||
|
var items = Subject.GetItems();
|
||||||
|
|
||||||
|
items.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy.GetTorrents does not return original url. So item has to be found via magnet url.
|
||||||
|
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
|
||||||
|
public void Download_should_get_hash_from_magnet_url(String magnetUrl, String expectedHash)
|
||||||
|
{
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
remoteEpisode.Release.DownloadUrl = magnetUrl;
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().Be(expectedHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Queued, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
|
||||||
|
public void GetItems_should_return_queued_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_queued.Status = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnQueuedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Queued)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
|
||||||
|
public void GetItems_should_return_downloading_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_downloading.Status = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnDownloadingItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued, false)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked, DownloadItemStatus.Completed, false)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Started, DownloadItemStatus.Completed, true)]
|
||||||
|
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Paused, DownloadItemStatus.Completed, true)]
|
||||||
|
public void GetItems_should_return_completed_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, Boolean expectedReadOnly)
|
||||||
|
{
|
||||||
|
_completed.Status = apiStatus;
|
||||||
|
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
item.IsReadOnly.Should().Be(expectedReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_status_with_outputdirs()
|
||||||
|
{
|
||||||
|
var configItems = new Dictionary<String, String>();
|
||||||
|
|
||||||
|
configItems.Add("dir_active_download_flag", "true");
|
||||||
|
configItems.Add("dir_active_download", @"C:\Downloads\Downloading\utorrent".AsOsAgnostic());
|
||||||
|
configItems.Add("dir_completed_download", @"C:\Downloads\Finished\utorrent".AsOsAgnostic());
|
||||||
|
configItems.Add("dir_completed_download_flag", "true");
|
||||||
|
configItems.Add("dir_add_label", "true");
|
||||||
|
|
||||||
|
Mocker.GetMock<IUTorrentProxy>()
|
||||||
|
.Setup(v => v.GetConfig(It.IsAny<UTorrentSettings>()))
|
||||||
|
.Returns(configItems);
|
||||||
|
|
||||||
|
var result = Subject.GetStatus();
|
||||||
|
|
||||||
|
result.IsLocalhost.Should().BeTrue();
|
||||||
|
result.OutputRootFolders.Should().NotBeNull();
|
||||||
|
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\utorrent\tv".AsOsAgnostic());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_combine_drive_letter()
|
||||||
|
{
|
||||||
|
WindowsOnly();
|
||||||
|
|
||||||
|
_completed.RootDownloadPath = "D:";
|
||||||
|
|
||||||
|
PrepareClientToReturnCompletedItem();
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.OutputPath.Should().Be(@"D:\" + _title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_handle_http_redirect_to_magnet()
|
||||||
|
{
|
||||||
|
GivenRedirectToMagnet();
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
|
||||||
|
var id = Subject.Download(remoteEpisode);
|
||||||
|
|
||||||
|
id.Should().NotBeNullOrEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1" ?>
|
||||||
|
<rss version="0.91">
|
||||||
|
<channel>
|
||||||
|
<ttl>10</ttl>
|
||||||
|
<title>BitMeTV.ORG</title>
|
||||||
|
<link>http://www.bitmetv.org</link>
|
||||||
|
<description>This is a private - by registration only - website. You can help keep it alive by donating: http://www.bitmetv.org/donate.php</description>
|
||||||
|
<language>en-usde</language>
|
||||||
|
<copyright>Copyright © 2004 - 2007 BitMeTV.ORG</copyright>
|
||||||
|
<webMaster>noreply@bitmetv.org</webMaster>
|
||||||
|
<image>
|
||||||
|
<title>BitMeTV.ORG</title>
|
||||||
|
<url>http://www.bitmetv.org/favicon.ico</url>
|
||||||
|
<link>http://www.bitmetv.org</link>
|
||||||
|
<width>16</width>
|
||||||
|
<height>16</height>
|
||||||
|
<description>This is a private - by registration only - website. You can help keep it alive by donating: http://www.bitmetv.org/donate.php</description>
|
||||||
|
</image>
|
||||||
|
<item>
|
||||||
|
<title>Total.Divas.S02E08.HDTV.x264-CRiMSON</title>
|
||||||
|
<link>http://www.bitmetv.org/download.php/12/Total.Divas.S02E08.HDTV.x264-CRiMSON.torrent</link>
|
||||||
|
<pubDate>Tue, 13 May 2014 17:04:29 -0000</pubDate>
|
||||||
|
<description>
|
||||||
|
Category: (Reality TV - Un-scripted)
|
||||||
|
Size: 376.71 MB
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Aqua.Teen.Hunger.Force.S10.INTERNAL.HDTV.x264-BitMeTV</title>
|
||||||
|
<link>http://www.bitmetv.org/download.php/34/Aqua.Teen.Hunger.Force.S10.INTERNAL.HDTV.x264-BitMeTV.torrent</link>
|
||||||
|
<pubDate>Tue, 13 May 2014 17:03:12 -0000</pubDate>
|
||||||
|
<description>
|
||||||
|
Category: (Adult Swim)
|
||||||
|
Size: 725.46 MB
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Antiques.Roadshow.US.S18E16.720p.HDTV.x264-BAJSKORV</title>
|
||||||
|
<link>http://www.bitmetv.org/download.php/56/Antiques.Roadshow.US.S18E16.720p.HDTV.x264-BAJSKORV.torrent</link>
|
||||||
|
<pubDate>Tue, 13 May 2014 16:47:05 -0000</pubDate>
|
||||||
|
<description>
|
||||||
|
Category: (Reality TV - Un-scripted)
|
||||||
|
Size: 960.15 MB
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Seth.Meyers.2014.05.12.Chris.O.Dowd-Emma.Roberts.HDTV.x264-CROOKS</title>
|
||||||
|
<link>http://www.bitmetv.org/download.php/78/Seth.Meyers.2014.05.12.Chris.O.Dowd-Emma.Roberts.HDTV.x264-CROOKS.torrent</link>
|
||||||
|
<pubDate>Tue, 13 May 2014 16:01:21 -0000</pubDate>
|
||||||
|
<description>
|
||||||
|
Category: Seth Meyers
|
||||||
|
Size: 301.31 MB
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>The.Mole.Australia.Season.4</title>
|
||||||
|
<link>http://www.bitmetv.org/download.php/910/The%20Mole%20Australia%20-%20Season%204.torrent</link>
|
||||||
|
<pubDate>Tue, 13 May 2014 15:52:54 -0000</pubDate>
|
||||||
|
<description>
|
||||||
|
Category: (Reality TV - Competitive)
|
||||||
|
Size: 2.13 GB
|
||||||
|
</description>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
@ -0,0 +1,40 @@
|
|||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>NyaaTorrents</title>
|
||||||
|
<link>http://www.nyaa.se/</link>
|
||||||
|
<atom:link href="http://www.nyaa.se/?page=rss" rel="self" type="application/rss+xml" />
|
||||||
|
<description></description>
|
||||||
|
<item>
|
||||||
|
<title>[TSRaws] Futsuu no Joshikousei ga [Locodol] Yattemita. #07 (TBS).ts</title>
|
||||||
|
<category>Raw Anime</category>
|
||||||
|
<link>http://www.nyaa.se/?page=download&tid=587750</link>
|
||||||
|
<guid>http://www.nyaa.se/?page=view&tid=587750</guid>
|
||||||
|
<description><![CDATA[1 seeder(s), 2 leecher(s), 0 download(s) - 2.35 GiB]]></description>
|
||||||
|
<pubDate>Thu, 14 Aug 2014 18:10:36 +0000</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>[JIGGYSUB] KOI KOI 7 EP07 [R2DVD 420P H264 AC3]</title>
|
||||||
|
<category>English-translated Anime</category>
|
||||||
|
<link>http://www.nyaa.se/?page=download&tid=587749</link>
|
||||||
|
<guid>http://www.nyaa.se/?page=view&tid=587749</guid>
|
||||||
|
<description><![CDATA[1 seeder(s), 2 leecher(s), 25 download(s) - 1.36 GiB]]></description>
|
||||||
|
<pubDate>Thu, 14 Aug 2014 18:05:22 +0000</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>[Ohys-Raws] RAIL WARS! - 07 (TBS 1280x720 x264 AAC).mp4</title>
|
||||||
|
<category>Raw Anime</category>
|
||||||
|
<link>http://www.nyaa.se/?page=download&tid=587748</link>
|
||||||
|
<guid>http://www.nyaa.se/?page=view&tid=587748</guid>
|
||||||
|
<description><![CDATA[2 seeder(s), 111 leecher(s), 243 download(s) - 424.2 MiB]]></description>
|
||||||
|
<pubDate>Thu, 14 Aug 2014 18:02:57 +0000</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>[Arabasma.com] Naruto Shippuuden - 372 [Arabic Sub] [MQ].mp4</title>
|
||||||
|
<category>Non-English-translated Anime</category>
|
||||||
|
<link>http://www.nyaa.se/?page=download&tid=587747</link>
|
||||||
|
<guid>http://www.nyaa.se/?page=view&tid=587747</guid>
|
||||||
|
<description><![CDATA[1 seeder(s), 0 leecher(s), 23 download(s) - 69.5 MiB]]></description>
|
||||||
|
<pubDate>Thu, 14 Aug 2014 18:01:36 +0000</pubDate>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
@ -0,0 +1,43 @@
|
|||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||||
|
{
|
||||||
|
public class TorrentSeedingSpecification : IDecisionEngineSpecification
|
||||||
|
{
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public TorrentSeedingSpecification(Logger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RejectionType Type
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return RejectionType.Permanent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
|
||||||
|
{
|
||||||
|
var torrentInfo = remoteEpisode.Release as TorrentInfo;
|
||||||
|
|
||||||
|
if (torrentInfo == null)
|
||||||
|
{
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrentInfo.Seeds != null && torrentInfo.Seeds < 1)
|
||||||
|
{
|
||||||
|
_logger.Debug("Not enough seeders. ({0})", torrentInfo.Seeds);
|
||||||
|
return Decision.Reject("Not enough seeders. ({0})", torrentInfo.Seeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue