diff --git a/Nuclex.Support (PC).csproj b/Nuclex.Support (PC).csproj
index f5b56eb..5141898 100644
--- a/Nuclex.Support (PC).csproj
+++ b/Nuclex.Support (PC).csproj
@@ -101,6 +101,15 @@
UnintrusivePriorityQueue.Test
UnintrusivePriorityQueue.cs
+
+ false
+ LicenseKey
+
+
+ false
+ LicenseKey.Test
+ LicenseKey.cs
+
false
BinarySerializer.Test
diff --git a/Source/Collections/ParentingCollection.cs b/Source/Collections/ParentingCollection.cs
index 9d0cd1f..a3a08c8 100644
--- a/Source/Collections/ParentingCollection.cs
+++ b/Source/Collections/ParentingCollection.cs
@@ -61,9 +61,9 @@ namespace Nuclex.Support.Collections {
/// Disposes the collection and optionally all items contained therein
/// Whether to try calling Dispose() on all items
///
- /// This method is intended to support collections that need to dispose their
+ /// This method is intended to support collections that need to dispose of their
/// items. The ParentingCollection will first detach all items from the parent
- /// object and them call Dispose() on any item that implements IDisposable.
+ /// object and then call Dispose() on any item that implements IDisposable.
///
protected void InternalDispose(bool disposeItems) {
diff --git a/Source/Licensing/LicenseKey.Test.cs b/Source/Licensing/LicenseKey.Test.cs
new file mode 100644
index 0000000..e89d2e5
--- /dev/null
+++ b/Source/Licensing/LicenseKey.Test.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections;
+
+#if UNITTEST
+
+using NUnit.Framework;
+
+namespace Nuclex.Licensing {
+
+ /// Unit test for the license key class
+ [TestFixture]
+ public class LicenseKeyTest {
+
+ /// Validates the correct translation of keys to GUIDs and back
+ [Test]
+ public void TestGuidKeyConversion() {
+
+ for(int i = 0; i < 128; ++i) {
+
+ // Create a new BitArray with the n.th bit set
+ BitArray guidBits = new BitArray(128);
+ guidBits[i] = true;
+
+ // Create a GUID from this Bitarray
+ byte[] guidBytes = new byte[16];
+ guidBits.CopyTo(guidBytes, 0);
+ Guid originalGuid = new Guid(guidBytes);
+
+ // Convert the GUID into a license key and back to a GUID
+ string licenseKey = new LicenseKey(originalGuid).ToString();
+ Guid rebuiltGuid = LicenseKey.Parse(licenseKey).ToGuid();
+
+ // Verify that the original GUID matches the fore-and-back converted one
+ Assert.AreEqual(originalGuid, rebuiltGuid, "Test for GUID bit " + i);
+
+ }
+
+ }
+
+ /// Tests whether license keys can be modified without destroying them
+ [Test]
+ public void TestKeyModification() {
+
+ for(int i = 0; i < 4; ++i) {
+ for(int j = 0; j < 8; ++j) {
+
+ LicenseKey testKey = new LicenseKey(
+ new Guid(-1, -1, -1, 255, 255, 255, 255, 255, 255, 255, 255)
+ );
+
+ string originalString = testKey.ToString();
+ testKey[i] &= ~(1 << j);
+ string modifiedString = testKey.ToString();
+
+ Assert.IsTrue(
+ originalString != modifiedString, "Modified string differs from original"
+ );
+
+ testKey[i] |= (1 << j);
+ string revertedString = testKey.ToString();
+
+ Assert.AreEqual(
+ originalString, revertedString, "Original state restorable"
+ );
+
+ } // for j
+ } // for i
+
+ }
+
+ }
+
+} // namespace Nuclex.Licensing
+
+#endif // UNITTEST
diff --git a/Source/Licensing/LicenseKey.cs b/Source/Licensing/LicenseKey.cs
new file mode 100644
index 0000000..408c5a4
--- /dev/null
+++ b/Source/Licensing/LicenseKey.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections;
+using System.IO;
+using System.Text;
+
+namespace Nuclex.Licensing {
+
+ /// Typical license key with 5x5 alphanumerical characters
+ ///
+ ///
+ /// This class manages a license key like it is used in Microsoft products.
+ /// Althought it is probably not the exact same technique used by Microsoft,
+ /// the textual representation of the license keys looks identical,
+ /// eg. O809J-RN5TD-IM3CU-4IG1O-O90X9
.
+ ///
+ ///
+ /// Available storage space is used efficiently and allows for up to four
+ /// 32 bit integers to be stored within the key, that's enough for a full GUID.
+ /// The four integers can be modified directly, for example to
+ /// store feature lists, checksums or other data within the key.
+ ///
+ ///
+ public class LicenseKey {
+
+ /// Parses the license key contained in a string
+ /// String containing a license key that is to be parsed
+ /// The license key parsed from provided string
+ public static LicenseKey Parse(string key) {
+ LicenseKey newKey = new LicenseKey();
+ newKey.parse(key);
+
+ return newKey;
+ }
+
+ /// Initializes a new, empty license key
+ public LicenseKey() : this(Guid.Empty) { }
+
+ /// Initializes the license key from a GUID
+ /// GUID that is used to create the license key
+ public LicenseKey(Guid source) {
+ guid = source;
+ }
+
+ /// Accesses the four integer values within a license key
+ public int this[int index] {
+ get {
+ if((index < 0) || (index > 3))
+ throw new IndexOutOfRangeException("Index out of range");
+
+ return BitConverter.ToInt32(guid.ToByteArray(), index * 4);
+ }
+ set {
+ if((index < 0) || (index > 3))
+ throw new IndexOutOfRangeException("Index out of range");
+
+ using(MemoryStream guidBytes = new MemoryStream(guid.ToByteArray())) {
+ guidBytes.Position = index * 4;
+ new BinaryWriter(guidBytes).Write(value);
+
+ guid = new Guid(guidBytes.ToArray());
+ }
+ }
+ }
+
+ /// Converts the license key into a GUID
+ /// The GUID created from the license key
+ public Guid ToGuid() {
+ return guid;
+ }
+
+ /// Converts the license key into a byte array
+ /// A byte array containing the converted license key
+ public byte[] ToByteArray() {
+ return guid.ToByteArray();
+ }
+
+ /// Converts the license key to a string
+ /// A string containing the converted license key
+ public override string ToString() {
+ StringBuilder resultBuilder = new StringBuilder();
+
+ // Build a bit array from the input data
+ BitArray bits = new BitArray(guid.ToByteArray());
+ mangle(bits);
+
+ int sequence = 0;
+
+ // Build 4 sequences of 6 characters from the first 124 bits
+ for(int i = 0; i < 4; ++i) {
+
+ // We take the next 31 bits from the buffer
+ for(int j = 0; j < 31; ++j)
+ sequence |= (int)powersOfTwo[j, bits[i * 31 + j] ? 1 : 0];
+
+ // Using 7 bits, a number up to 2.147.483.648 can be represented,
+ // while 6 alpha-numerical characters allow for 2.176.782.336 possible values,
+ // which is enough to fit 7 bits into each 6 alpha-numerical characters
+ // nun in 6 alphanumerische Zeichen zu verpacken.
+ for(int j = 0; j < 6; ++j) {
+ resultBuilder.Append(codeTable[sequence % 36]);
+ sequence /= 36;
+ }
+
+ }
+
+ // Use the remaining 4 bits to build the final character
+ resultBuilder.Append(
+ codeTable[
+ (int)(
+ powersOfTwo[4, bits[124] ? 1 : 0] |
+ powersOfTwo[3, bits[125] ? 1 : 0] |
+ powersOfTwo[2, bits[126] ? 1 : 0] |
+ powersOfTwo[1, bits[127] ? 1 : 0] |
+ powersOfTwo[0, 1] // One bit remains unused :)
+ )
+ ]
+ );
+
+ // Now build a nice, readable string from the decoded characters
+ string s = resultBuilder.ToString();
+ return
+ s.Substring(0, 5) + "-" +
+ s.Substring(5, 5) + "-" +
+ s.Substring(10, 5) + "-" +
+ s.Substring(15, 5) + "-" +
+ s.Substring(20, 5);
+ }
+
+ /// Mangles a bit array
+ /// Bit array that will be mangled
+ private static void mangle(BitArray bits) {
+ BitArray temp = new BitArray(bits);
+
+ for(int i = 0; i < temp.Length; ++i) {
+ bits[i] = temp[shuffle[i]];
+
+ if((i & 1) != 0)
+ bits[i] = !bits[i];
+ }
+ }
+
+ /// Unmangles a bit array
+ /// Bit array that will be unmangled
+ private static void unmangle(BitArray bits) {
+ BitArray temp = new BitArray(bits);
+
+ for(int i = 0; i < temp.Length; ++i) {
+ if((i & 1) != 0)
+ temp[i] = !temp[i];
+
+ bits[shuffle[i]] = temp[i];
+ }
+ }
+
+ /// Parses a license key from a string
+ /// String the license key is read from
+ /// The license key parsed from the provided string
+ private void parse(string key) {
+ key = key.Replace(" ", string.Empty).Replace("-", string.Empty).ToUpper();
+ if(key.Length != 25)
+ throw new ArgumentException("This is not a license key");
+
+ BitArray bits = new BitArray(128);
+ uint sequence;
+
+ // Convert the first 4 sequences of 6 chars into 124 bits
+ for(int j = 0; j < 4; j++) {
+
+ sequence =
+ (uint)codeTable.IndexOf(key[j * 6 + 5]) * 60466176 +
+ (uint)codeTable.IndexOf(key[j * 6 + 4]) * 1679616 +
+ (uint)codeTable.IndexOf(key[j * 6 + 3]) * 46656 +
+ (uint)codeTable.IndexOf(key[j * 6 + 2]) * 1296 +
+ (uint)codeTable.IndexOf(key[j * 6 + 1]) * 36 +
+ (uint)codeTable.IndexOf(key[j * 6 + 0]);
+
+ for(int i = 0; i < 31; i++)
+ bits[j * 31 + i] = (sequence & powersOfTwo[i, 1]) != 0;
+
+ }
+
+ // Append the remaining character's 4 bits
+ sequence = (uint)codeTable.IndexOf(key[24]);
+ bits[124] = (sequence & powersOfTwo[4, 1]) != 0;
+ bits[125] = (sequence & powersOfTwo[3, 1]) != 0;
+ bits[126] = (sequence & powersOfTwo[2, 1]) != 0;
+ bits[127] = (sequence & powersOfTwo[1, 1]) != 0;
+
+ // Revert the mangling that was applied to the key when encoding...
+ unmangle(bits);
+
+ // ...and we've got our GUID back!
+ byte[] guidBytes = new byte[16];
+ bits.CopyTo(guidBytes, 0);
+ guid = new Guid(guidBytes);
+ }
+
+ /// Table with the individual characters in a key
+ private static readonly string codeTable =
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ /// Helper array with the powers of two
+ private static readonly uint[,] powersOfTwo = new uint[32, 2] {
+ { 0, 1 }, { 0, 2 }, { 0, 4 }, { 0, 8 },
+ { 0, 16 }, { 0, 32 }, { 0, 64 }, { 0, 128 },
+ { 0, 256 }, { 0, 512 }, { 0, 1024 }, { 0, 2048 },
+ { 0, 4096 }, { 0, 8192 }, { 0, 16384 }, { 0, 32768 },
+ { 0, 65536 }, { 0, 131072 }, { 0, 262144 }, { 0, 524288 },
+ { 0, 1048576 }, { 0, 2097152 }, { 0, 4194304 }, { 0, 8388608 },
+ { 0, 16777216 }, { 0, 33554432 }, { 0, 67108864 }, { 0, 134217728 },
+ { 0, 268435456 }, { 0, 536870912 }, { 0, 1073741824 }, { 0, 2147483648 }
+ };
+
+ /// Index list for rotating the bit arrays
+ private static readonly byte[] shuffle = new byte[128] {
+ 99, 47, 19, 104, 40, 71, 35, 82, 88, 2, 117, 118, 105, 42, 84, 48,
+ 33, 54, 43, 27, 78, 53, 61, 50, 109, 87, 69, 66, 25, 76, 45, 14,
+ 92, 16, 123, 98, 95, 37, 34, 8, 1, 49, 20, 90, 15, 97, 22, 108,
+ 5, 32, 120, 106, 122, 70, 67, 55, 46, 89, 100, 0, 26, 94, 121, 7,
+ 56, 59, 103, 79, 107, 36, 125, 119, 126, 44, 18, 93, 75, 116, 31, 9,
+ 73, 113, 3, 41, 124, 60, 77, 91, 28, 114, 65, 12, 39, 127, 72, 17,
+ 112, 21, 96, 111, 83, 101, 85, 80, 23, 68, 57, 13, 4, 10, 51, 63,
+ 11, 30, 115, 102, 86, 81, 74, 110, 62, 38, 29, 64, 52, 6, 24, 58
+ };
+
+ /// GUID in which the key is stored
+ private Guid guid;
+
+ }
+
+} // namespace Nuclex.Licensing