Initial Commit.
Initial Commit.

--- /dev/null
+++ b/KerbalStuffWrapper.sln
@@ -1,1 +1,21 @@
+
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KerbalStuffWrapper", "KerbalStuffWrapper\KerbalStuffWrapper.csproj", "{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|x86 = Debug|x86
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Debug|x86.ActiveCfg = Debug|x86
+		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Debug|x86.Build.0 = Debug|x86
+		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Release|x86.ActiveCfg = Release|x86
+		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Release|x86.Build.0 = Release|x86
+	EndGlobalSection
+	GlobalSection(MonoDevelopProperties) = preSolution
+		StartupItem = KerbalStuffWrapper\KerbalStuffWrapper.csproj
+	EndGlobalSection
+EndGlobal
 

--- /dev/null
+++ b/KerbalStuffWrapper/FormUpload.cs
@@ -1,1 +1,132 @@
+// Implements multipart/form-data POST in C# http://www.ietf.org/rfc/rfc2388.txt
+// http://www.briangrinstead.com/blog/multipart-form-post-in-c
 
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+
+public static class FormUpload
+{
+	private static readonly Encoding encoding = Encoding.UTF8;
+
+	public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters)
+	{
+		return MultipartFormDataPost(postUrl, userAgent, postParameters, new CookieContainer());
+	}
+
+	public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters, CookieContainer cookies)
+	{
+		string formDataBoundary = String.Format("----------{0:N}", Guid.NewGuid());
+		string contentType = "multipart/form-data; boundary=" + formDataBoundary;
+
+		byte[] formData = GetMultipartFormData(postParameters, formDataBoundary);
+
+		return PostForm(postUrl, userAgent, contentType, formData, cookies);
+	}
+
+	private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData)
+	{
+		return PostForm(postUrl, userAgent, contentType, formData, new CookieContainer());
+	}
+
+	private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData, CookieContainer cookies)
+	{
+		HttpWebRequest request = WebRequest.Create(postUrl) as HttpWebRequest;
+
+		if (request == null)
+		{
+			throw new NullReferenceException("request is not a http request");
+		}
+
+		// Set up the request properties.
+		request.Method = "POST";
+		request.ContentType = contentType;
+		request.UserAgent = userAgent;
+		request.CookieContainer = cookies;
+		request.ContentLength = formData.Length;
+
+		// You could add authentication here as well if needed:
+		// request.PreAuthenticate = true;
+		// request.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequested;
+		// request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(System.Text.Encoding.Default.GetBytes("username" + ":" + "password")));
+
+		// Send the form data to the request.
+		using (Stream requestStream = request.GetRequestStream())
+		{
+			requestStream.Write(formData, 0, formData.Length);
+			requestStream.Close();
+		}
+
+		return request.GetResponse() as HttpWebResponse;
+	}
+
+	private static byte[] GetMultipartFormData(Dictionary<string, object> postParameters, string boundary)
+	{
+		Stream formDataStream = new System.IO.MemoryStream();
+		bool needsCLRF = false;
+
+		foreach (var param in postParameters)
+		{
+			// Thanks to feedback from commenters, add a CRLF to allow multiple parameters to be added.
+			// Skip it on the first parameter, add it to subsequent parameters.
+			if (needsCLRF)
+				formDataStream.Write(encoding.GetBytes("\r\n"), 0, encoding.GetByteCount("\r\n"));
+
+			needsCLRF = true;
+
+			if (param.Value is FileParameter)
+			{
+				FileParameter fileToUpload = (FileParameter)param.Value;
+
+				// Add just the first part of this param, since we will write the file data directly to the Stream
+				string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n",
+					boundary,
+					param.Key,
+					fileToUpload.FileName ?? param.Key,
+					fileToUpload.ContentType ?? "application/octet-stream");
+
+				formDataStream.Write(encoding.GetBytes(header), 0, encoding.GetByteCount(header));
+
+				// Write the file data directly to the Stream, rather than serializing it to a string.
+				formDataStream.Write(fileToUpload.File, 0, fileToUpload.File.Length);
+			}
+			else
+			{
+				string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}",
+					boundary,
+					param.Key,
+					param.Value);
+				formDataStream.Write(encoding.GetBytes(postData), 0, encoding.GetByteCount(postData));
+			}
+		}
+
+		// Add the end of the request.  Start with a newline
+		string footer = "\r\n--" + boundary + "--\r\n";
+		formDataStream.Write(encoding.GetBytes(footer), 0, encoding.GetByteCount(footer));
+
+		// Dump the Stream into a byte[]
+		formDataStream.Position = 0;
+		byte[] formData = new byte[formDataStream.Length];
+		formDataStream.Read(formData, 0, formData.Length);
+		formDataStream.Close();
+
+		return formData;
+	}
+
+	public class FileParameter
+	{
+		public byte[] File { get; set; }
+		public string FileName { get; set; }
+		public string ContentType { get; set; }
+		public FileParameter(byte[] file) : this(file, null) { }
+		public FileParameter(byte[] file, string filename) : this(file, filename, null) { }
+		public FileParameter(byte[] file, string filename, string contenttype)
+		{
+			File = file;
+			FileName = filename;
+			ContentType = contenttype;
+		}
+	}
+}

--- /dev/null
+++ b/KerbalStuffWrapper/KerbalStuff.cs
@@ -1,1 +1,373 @@
-
+using MiniJSON;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace KerbalStuff
+{
+	public static class KerbalStuff
+	{
+		public const string RootUri = "https://kerbalstuff.com";
+
+		public const string APIUri = RootUri + "/api";
+
+		public const string UserAgent = "KerbalStuffWrapper by toadicus";
+
+		public static void Login(string username, string password)
+		{
+			Dictionary<string, object> postParams = new Dictionary<string, object>();
+			postParams.Add("username", username);
+			postParams.Add("password", password);
+
+			HttpWebResponse response = FormUpload.MultipartFormDataPost(
+				KerbalStuffAction.Login.UriFormat,
+				"KerbalStuffWrapper by toadicus",
+				postParams
+			);
+
+			cookies = response.Cookies;
+		}
+
+		public static string Create(Mod mod, string fileName, string filePath)
+		{
+			if (mod == null)
+			{
+				throw new ArgumentNullException("KerbalStuffWrapper.Create: mod argument cannot be null.");
+			}
+			else
+			{
+				if (mod.name == string.Empty)
+					throw new ArgumentException("mod.name cannot be empty.");
+				if (mod.license == string.Empty)
+					throw new ArgumentException("mod.license cannot be empty.");
+				if (mod.short_description == string.Empty)
+					throw new ArgumentException("mod.short_description cannot be empty.");
+				if (mod.versions.Count < 1)
+					throw new ArgumentException("mod must have a single version to create.");
+				else if (mod.versions[0] == null)
+				{
+					throw new ArgumentNullException("mod.versions[0] cannot be null.");
+				}
+				else
+				{
+					if (mod.versions[0].friendly_version == string.Empty)
+						throw new ArgumentException("mod.versions[0].friendly_version cannot be empty.");
+					if (mod.versions[0].ksp_version == string.Empty)
+						throw new ArgumentException("mod.versions[0].ksp_version cannot be empty.");
+				}
+			}
+
+			if (cookies == null)
+			{
+				throw new Exception("KerbalStuffWrapper.Create: Must log in first.");
+			}
+
+			if (!File.Exists(filePath))
+			{
+				throw new IOException(string.Format("KerbalStuffWrapper.Create: File '{0}' does not exist.", filePath));
+			}
+
+			Dictionary<string, object> postParams = new Dictionary<string, object>();
+			postParams.Add("name", mod.name);
+			postParams.Add("short-description", mod.short_description);
+			postParams.Add("license", mod.license);
+			postParams.Add("version", mod.versions[0].friendly_version);
+			postParams.Add("ksp-version", mod.versions[0].ksp_version);
+			postParams.Add("zipball", ReadZipballParameter(fileName, filePath));
+
+			ExecutePostRequest(KerbalStuffAction.Create.UriFormat, postParams, cookies);
+
+			return string.Concat(RootUri, (currentJson as Dictionary<string, object>)["url"]);
+		}
+
+		public static string Update(long modId, ModVersion version, bool notifyFollowers, string fileName, string filePath)
+		{
+			if (version == null)
+			{
+				throw new ArgumentNullException("KerbalStuffWrapper.Update: version cannot be null");
+			}
+			if (version.friendly_version == string.Empty)
+				throw new ArgumentException("KerbalStuffWrapper.Update: version.friendly_version cannot be empty");
+			if (version.ksp_version == string.Empty)
+				throw new ArgumentException("KerbalStuffWrapper.Update: version.ksp_version cannot be empty");
+
+			if (cookies == null)
+			{
+				throw new Exception("KerbalStuffWrapper.Update: Must log in first.");
+			}
+
+			if (!File.Exists(filePath))
+			{
+				throw new IOException(string.Format("KerbalStuffWrapper.Update: File '{0}' does not exist.", filePath));
+			}
+
+			string uri = string.Format(KerbalStuffAction.Update.UriFormat, modId);
+
+			Dictionary<string, object> postParams = new Dictionary<string, object>();
+			postParams.Add("version", version.friendly_version);
+			postParams.Add("ksp-version", version.ksp_version);
+
+			if (version.changelog != null && version.changelog != string.Empty)
+			{
+				postParams.Add("changelog", version.changelog);
+			}
+
+			postParams.Add("notify-followers", notifyFollowers ? "yes" : "no");
+
+			postParams.Add("zipball", ReadZipballParameter(fileName, filePath));
+
+			ExecutePostRequest(uri, postParams, cookies);
+
+			return string.Concat(RootUri, (currentJson as Dictionary<string, object>)["url"]);
+		}
+
+		public static Mod ModInfo(long modId)
+		{
+			string uri = string.Format(KerbalStuffAction.ModInfo.UriFormat, modId);
+
+			ExecuteGetRequest(uri, KerbalStuffAction.ModInfo.RequestMethod, false);
+
+			Mod mod = null;
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				mod = new Mod(currentJson as Dictionary<string, object>);
+			}
+
+			return mod;
+		}
+
+		public static ModVersion ModLatest(long modId)
+		{
+			string uri = string.Format(KerbalStuffAction.ModLatest.UriFormat, modId);
+
+			ExecuteGetRequest(uri, KerbalStuffAction.ModLatest.RequestMethod, false);
+
+			ModVersion ver = null;
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				ver = new ModVersion(currentJson as Dictionary<string, object>);
+			}
+
+			return ver;
+		}
+
+		public static List<Mod> ModSearch(string query)
+		{
+			string uri = string.Format(KerbalStuffAction.ModSearch.UriFormat, query);
+
+			ExecuteGetRequest(uri, KerbalStuffAction.ModSearch.RequestMethod, false);
+
+			List<Mod> rList = new List<Mod>();
+
+			if (currentJson != null && currentJson is List<object>)
+			{
+				foreach (var modObj in (currentJson as List<object>))
+				{
+					if (modObj is Dictionary<string, object>)
+					{
+						rList.Add(new Mod(modObj as Dictionary<string, object>));
+					}
+				}
+			}
+
+			return rList;
+		}
+
+		public static User UserInfo(string username)
+		{
+			ExecuteGetRequest(KerbalStuffAction.UserInfo, false, username);
+
+
+			User user = null;
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				user = new User(currentJson);
+			}
+
+			return user;
+		}
+
+		public static List<User> UserSearch(string query)
+		{
+			ExecuteGetRequest(KerbalStuffAction.UserSearch, false, query);
+
+			List<User> users = new List<User>();
+
+			if (currentJson != null && currentJson is List<object>)
+			{
+				foreach (object userObj in (currentJson as List<object>))
+				{
+					users.Add(new User(userObj));
+				}
+			}
+
+			return users;
+		}
+
+		private static HttpWebRequest currentRequest;
+		private static HttpWebResponse currentResponse;
+		private static CookieCollection cookies;
+
+		private static object currentJson;
+
+		private static void ExecuteGetRequest(KerbalStuffAction action, bool assignCookies, params object[] formatArgs)
+		{
+			string uri = string.Format(action.UriFormat, formatArgs);
+
+			ExecuteGetRequest(uri, action.RequestMethod, assignCookies);
+		}
+
+		private static void ExecuteGetRequest(string uri, string method, bool assignCookies, byte[] formData = null)
+		{
+			currentJson = null;
+			currentRequest = null;
+			currentResponse = null;
+
+			if (uri == string.Empty)
+			{
+				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: uri must not be empty.");
+			}
+
+			uri = Uri.EscapeUriString(uri);
+
+			method = method.ToUpper();
+
+			if (method != "POST" && method != "GET")
+			{
+				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: method must be POST or GET.");
+			}
+
+			currentRequest = (HttpWebRequest)WebRequest.Create(uri);
+			currentRequest.Method = method;
+
+			Console.WriteLine("Request cookies: " + currentRequest.CookieContainer);
+
+			currentResponse = (HttpWebResponse)currentRequest.GetResponse();
+
+			Console.WriteLine("Response cookies: " + string.Join(
+					",",
+					currentResponse.Cookies.Cast<Cookie>().Select(c => c.ToString()).ToArray()
+				));
+
+			if (currentResponse.StatusCode == HttpStatusCode.NotFound)
+			{
+				throw new WebException(string.Format("KerbalStuffWrapper.ExecuteRequest: URI not found: {0}", uri));
+			}
+
+			if (currentResponse.ContentType == "application/json")
+			{
+				var responseReader = new StreamReader(currentResponse.GetResponseStream());
+
+				string json = responseReader.ReadToEnd();
+
+				currentJson = Json.Deserialize(json);
+			}
+		}
+
+		private static void ExecutePostRequest(string uri, Dictionary<string, object> postParams, CookieCollection cookieCollection = null)
+		{
+			currentJson = null;
+			currentRequest = null;
+			currentResponse = null;
+
+			CookieContainer jar = new CookieContainer();
+
+			if (cookieCollection != null)
+			{
+				jar.Add(cookieCollection);
+			}
+
+			try
+			{
+				currentResponse = FormUpload.MultipartFormDataPost(
+					uri,
+					/*"http://toad.homelinux.net/post_dump.php",*/
+					"KerbalStuffWrapper by toadicus",
+					postParams,
+					jar
+				);
+
+				currentJson = Json.Deserialize((new StreamReader(currentResponse.GetResponseStream())).ReadToEnd());
+			}
+			catch (WebException ex)
+			{
+				Console.WriteLine(string.Format("KerbalStuffWrapper.Create: Caught WebException.  Response: {0}", (new StreamReader(ex.Response.GetResponseStream())).ReadToEnd()));
+			}
+		}
+
+		private static FormUpload.FileParameter ReadZipballParameter(string fileName, string filePath)
+		{
+			using (FileStream file = File.OpenRead(filePath))
+			{
+				byte[] buffer = new byte[1 << 16];
+				int bytesRead;
+
+				MemoryStream stream = new MemoryStream();
+
+				while ((bytesRead = file.Read(buffer, 0, buffer.Length)) > 0)
+				{
+					stream.Write(buffer, 0, bytesRead);
+				}
+
+				byte[] fileBytes = stream.GetBuffer();
+
+				return new FormUpload.FileParameter(fileBytes, fileName, "application/zip");
+			}
+
+			return null;
+		}
+	}
+
+	public struct KerbalStuffAction
+	{
+		public static readonly KerbalStuffAction Create = new KerbalStuffAction("create", "/mod/create", "POST");
+		public static readonly KerbalStuffAction Login = new KerbalStuffAction("login", "/login", "POST");
+		public static readonly KerbalStuffAction ModInfo = new KerbalStuffAction("modinfo", "/mod/{0:d}", "GET");
+		public static readonly KerbalStuffAction ModLatest = new KerbalStuffAction(
+			"modlatest",
+			"/mod/{0:d}/latest",
+			"GET"
+		);
+		public static readonly KerbalStuffAction ModSearch = new KerbalStuffAction(
+			"modsearch",
+			"/search/mod?query={0}",
+			"GET"
+		);
+		public static readonly KerbalStuffAction Update = new KerbalStuffAction("update", "/mod/{0:d}/update", "POST");
+		public static readonly KerbalStuffAction UserInfo = new KerbalStuffAction("userinfo", "/user/{0}", "GET");
+		public static readonly KerbalStuffAction UserSearch = new KerbalStuffAction(
+			"usersearch",
+			"/search/user?query={0}",
+			"GET"
+		);
+
+		public string Action;
+
+		public string UriPathFormat;
+
+		public string RequestMethod;
+
+		public string UriFormat
+		{
+			get
+			{
+				return string.Format("{0}{1}", KerbalStuff.APIUri, this.UriPathFormat);
+			}
+		}
+
+		public KerbalStuffAction(string action, string uriFormat, string requestMethod) : this()
+		{
+			this.Action = action;
+			this.UriPathFormat = uriFormat;
+			this.RequestMethod = requestMethod;
+		}
+	}
+}
+
+

--- /dev/null
+++ b/KerbalStuffWrapper/KerbalStuffWrapper.cs
@@ -1,1 +1,95 @@
+using MiniJSON;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
 
+namespace KerbalStuffWrapper
+{
+	public static class KerbalStuffWrapper
+	{
+		private static HttpWebRequest currentRequest;
+		private static HttpWebResponse currentResponse;
+		private static CookieContainer cookies;
+
+		private static void ExecuteRequest(string uri, string method, bool assignCookies)
+		{
+			if (uri == string.Empty)
+			{
+				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: uri must not be empty.");
+			}
+
+			method = method.ToUpper();
+
+			if (method != "POST" && method != "GET")
+			{
+				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: method must be POST or GET.");
+			}
+
+			currentRequest = (HttpWebRequest)WebRequest.Create(uri);
+			currentRequest.Method = method;
+
+			if (assignCookies)
+			{
+				if (cookies == null)
+				{
+					throw new ArgumentNullException("KerbalStuffWrapper.ExecuteRequest: cookies must not be null.");
+				}
+
+				currentRequest.CookieContainer = cookies;
+			}
+
+
+		}
+
+		public static List<Dictionary<string, object>> Search(string query)
+		{
+			// ServicePointManager.ServerCertificateValidationCallback
+			string uri = string.Format(KerbalStuffAction.Search.UriFormat, query);
+
+			HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
+			req.Method = KerbalStuffAction.Search.RequestMethod;
+
+			var responseReader = new StreamReader(req.GetResponse().GetResponseStream());
+
+			string json = responseReader.ReadToEnd();
+
+			Console.WriteLine(json);
+
+			object jsonObj = Json.Deserialize(json);
+
+			return (List<Dictionary<string, object>>)jsonObj;
+		}
+	}
+
+	public struct KerbalStuffAction
+	{
+		public static readonly KerbalStuffAction Search = new KerbalStuffAction("search", "search/mod?query=%s", "GET");
+		public static readonly KerbalStuffAction ModInfo = new KerbalStuffAction("modinfo", "mod/%d", "GET");
+
+		public const string APIUri = "https://www.kerbalstuff.com/api/";
+
+		public string Action;
+
+		public string UriPathFormat;
+
+		public string RequestMethod;
+
+		public string UriFormat
+		{
+			get
+			{
+				return string.Format("{0}{1}", APIUri, this.UriPathFormat);
+			}
+		}
+
+		public KerbalStuffAction(string action, string uriFormat, string requestMethod) : this()
+		{
+			this.Action = action;
+			this.UriPathFormat = uriFormat;
+			this.RequestMethod = requestMethod;
+		}
+	}
+}
+
+

--- /dev/null
+++ b/KerbalStuffWrapper/KerbalStuffWrapper.csproj
@@ -1,1 +1,48 @@
-
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <RootNamespace>KerbalStuff</RootNamespace>
+    <AssemblyName>KerbalStuffWrapper</AssemblyName>
+    <UseMSBuildEngine>False</UseMSBuildEngine>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <PlatformTarget>x86</PlatformTarget>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
+    <DebugType>full</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <PlatformTarget>x86</PlatformTarget>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Net" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Program.cs" />
+    <Compile Include="MiniJSON.cs" />
+    <Compile Include="Mod.cs" />
+    <Compile Include="KerbalStuff.cs" />
+    <Compile Include="User.cs" />
+    <Compile Include="FormUpload.cs" />
+  </ItemGroup>
+</Project>

--- /dev/null
+++ b/KerbalStuffWrapper/MiniJSON.cs
@@ -1,1 +1,548 @@
-
+/*
+ * Copyright (c) 2013 Calvin Rien
+ *
+ * Based on the JSON parser by Patrick van Bergen
+ * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
+ *
+ * Simplified it so that it doesn't throw exceptions
+ * and can be used in Unity iPhone with maximum code stripping.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace MiniJSON {
+	// Example usage:
+	//
+	//  using UnityEngine;
+	//  using System.Collections;
+	//  using System.Collections.Generic;
+	//  using MiniJSON;
+	//
+	//  public class MiniJSONTest : MonoBehaviour {
+	//      void Start () {
+	//          var jsonString = "{ \"array\": [1.44,2,3], " +
+	//                          "\"object\": {\"key1\":\"value1\", \"key2\":256}, " +
+	//                          "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " +
+	//                          "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " +
+	//                          "\"int\": 65536, " +
+	//                          "\"float\": 3.1415926, " +
+	//                          "\"bool\": true, " +
+	//                          "\"null\": null }";
+	//
+	//          var dict = Json.Deserialize(jsonString) as Dictionary<string,object>;
+	//
+	//          Debug.Log("deserialized: " + dict.GetType());
+	//          Debug.Log("dict['array'][0]: " + ((List<object>) dict["array"])[0]);
+	//          Debug.Log("dict['string']: " + (string) dict["string"]);
+	//          Debug.Log("dict['float']: " + (double) dict["float"]); // floats come out as doubles
+	//          Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs
+	//          Debug.Log("dict['unicode']: " + (string) dict["unicode"]);
+	//
+	//          var str = Json.Serialize(dict);
+	//
+	//          Debug.Log("serialized: " + str);
+	//      }
+	//  }
+
+	/// <summary>
+	/// This class encodes and decodes JSON strings.
+	/// Spec. details, see http://www.json.org/
+	///
+	/// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary.
+	/// All numbers are parsed to doubles.
+	/// </summary>
+	public static class Json {
+		/// <summary>
+		/// Parses the string json into a value
+		/// </summary>
+		/// <param name="json">A JSON string.</param>
+		/// <returns>An List&lt;object&gt;, a Dictionary&lt;string, object&gt;, a double, an integer,a string, null, true, or false</returns>
+		public static object Deserialize(string json) {
+			// save the string for debug information
+			if (json == null) {
+				return null;
+			}
+
+			return Parser.Parse(json);
+		}
+
+		sealed class Parser : IDisposable {
+			const string WORD_BREAK = "{}[],:\"";
+
+			public static bool IsWordBreak(char c) {
+				return Char.IsWhiteSpace(c) || WORD_BREAK.IndexOf(c) != -1;
+			}
+
+			enum TOKEN {
+				NONE,
+				CURLY_OPEN,
+				CURLY_CLOSE,
+				SQUARED_OPEN,
+				SQUARED_CLOSE,
+				COLON,
+				COMMA,
+				STRING,
+				NUMBER,
+				TRUE,
+				FALSE,
+				NULL
+			};
+
+			StringReader json;
+
+			Parser(string jsonString) {
+				json = new StringReader(jsonString);
+			}
+
+			public static object Parse(string jsonString) {
+				using (var instance = new Parser(jsonString)) {
+					return instance.ParseValue();
+				}
+			}
+
+			public void Dispose() {
+				json.Dispose();
+				json = null;
+			}
+
+			Dictionary<string, object> ParseObject() {
+				Dictionary<string, object> table = new Dictionary<string, object>();
+
+				// ditch opening brace
+				json.Read();
+
+				// {
+				while (true) {
+					switch (NextToken) {
+						case TOKEN.NONE:
+							return null;
+						case TOKEN.COMMA:
+							continue;
+						case TOKEN.CURLY_CLOSE:
+							return table;
+						default:
+							// name
+							string name = ParseString();
+							if (name == null) {
+								return null;
+							}
+
+							// :
+							if (NextToken != TOKEN.COLON) {
+								return null;
+							}
+							// ditch the colon
+							json.Read();
+
+							// value
+							table[name] = ParseValue();
+							break;
+					}
+				}
+			}
+
+			List<object> ParseArray() {
+				List<object> array = new List<object>();
+
+				// ditch opening bracket
+				json.Read();
+
+				// [
+				var parsing = true;
+				while (parsing) {
+					TOKEN nextToken = NextToken;
+
+					switch (nextToken) {
+						case TOKEN.NONE:
+							return null;
+						case TOKEN.COMMA:
+							continue;
+						case TOKEN.SQUARED_CLOSE:
+							parsing = false;
+							break;
+						default:
+							object value = ParseByToken(nextToken);
+
+							array.Add(value);
+							break;
+					}
+				}
+
+				return array;
+			}
+
+			object ParseValue() {
+				TOKEN nextToken = NextToken;
+				return ParseByToken(nextToken);
+			}
+
+			object ParseByToken(TOKEN token) {
+				switch (token) {
+					case TOKEN.STRING:
+						return ParseString();
+					case TOKEN.NUMBER:
+						return ParseNumber();
+					case TOKEN.CURLY_OPEN:
+						return ParseObject();
+					case TOKEN.SQUARED_OPEN:
+						return ParseArray();
+					case TOKEN.TRUE:
+						return true;
+					case TOKEN.FALSE:
+						return false;
+					case TOKEN.NULL:
+						return null;
+					default:
+						return null;
+				}
+			}
+
+			string ParseString() {
+				StringBuilder s = new StringBuilder();
+				char c;
+
+				// ditch opening quote
+				json.Read();
+
+				bool parsing = true;
+				while (parsing) {
+
+					if (json.Peek() == -1) {
+						parsing = false;
+						break;
+					}
+
+					c = NextChar;
+					switch (c) {
+						case '"':
+							parsing = false;
+							break;
+						case '\\':
+							if (json.Peek() == -1) {
+								parsing = false;
+								break;
+							}
+
+							c = NextChar;
+							switch (c) {
+								case '"':
+								case '\\':
+								case '/':
+									s.Append(c);
+									break;
+								case 'b':
+									s.Append('\b');
+									break;
+								case 'f':
+									s.Append('\f');
+									break;
+								case 'n':
+									s.Append('\n');
+									break;
+								case 'r':
+									s.Append('\r');
+									break;
+								case 't':
+									s.Append('\t');
+									break;
+								case 'u':
+									var hex = new char[4];
+
+									for (int i=0; i< 4; i++) {
+										hex[i] = NextChar;
+									}
+
+									s.Append((char) Convert.ToInt32(new string(hex), 16));
+									break;
+							}
+							break;
+						default:
+							s.Append(c);
+							break;
+					}
+				}
+
+				return s.ToString();
+			}
+
+			object ParseNumber() {
+				string number = NextWord;
+
+				if (number.IndexOf('.') == -1) {
+					long parsedInt;
+					Int64.TryParse(number, out parsedInt);
+					return parsedInt;
+				}
+
+				double parsedDouble;
+				Double.TryParse(number, out parsedDouble);
+				return parsedDouble;
+			}
+
+			void EatWhitespace() {
+				while (Char.IsWhiteSpace(PeekChar)) {
+					json.Read();
+
+					if (json.Peek() == -1) {
+						break;
+					}
+				}
+			}
+
+			char PeekChar {
+				get {
+					return Convert.ToChar(json.Peek());
+				}
+			}
+
+			char NextChar {
+				get {
+					return Convert.ToChar(json.Read());
+				}
+			}
+
+			string NextWord {
+				get {
+					StringBuilder word = new StringBuilder();
+
+					while (!IsWordBreak(PeekChar)) {
+						word.Append(NextChar);
+
+						if (json.Peek() == -1) {
+							break;
+						}
+					}
+
+					return word.ToString();
+				}
+			}
+
+			TOKEN NextToken {
+				get {
+					EatWhitespace();
+
+					if (json.Peek() == -1) {
+						return TOKEN.NONE;
+					}
+
+					switch (PeekChar) {
+						case '{':
+							return TOKEN.CURLY_OPEN;
+						case '}':
+							json.Read();
+							return TOKEN.CURLY_CLOSE;
+						case '[':
+							return TOKEN.SQUARED_OPEN;
+						case ']':
+							json.Read();
+							return TOKEN.SQUARED_CLOSE;
+						case ',':
+							json.Read();
+							return TOKEN.COMMA;
+						case '"':
+							return TOKEN.STRING;
+						case ':':
+							return TOKEN.COLON;
+						case '0':
+						case '1':
+						case '2':
+						case '3':
+						case '4':
+						case '5':
+						case '6':
+						case '7':
+						case '8':
+						case '9':
+						case '-':
+							return TOKEN.NUMBER;
+					}
+
+					switch (NextWord) {
+						case "false":
+							return TOKEN.FALSE;
+						case "true":
+							return TOKEN.TRUE;
+						case "null":
+							return TOKEN.NULL;
+					}
+
+					return TOKEN.NONE;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string
+		/// </summary>
+		/// <param name="json">A Dictionary&lt;string, object&gt; / List&lt;object&gt;</param>
+		/// <returns>A JSON encoded string, or null if object 'json' is not serializable</returns>
+		public static string Serialize(object obj) {
+			return Serializer.Serialize(obj);
+		}
+
+		sealed class Serializer {
+			StringBuilder builder;
+
+			Serializer() {
+				builder = new StringBuilder();
+			}
+
+			public static string Serialize(object obj) {
+				var instance = new Serializer();
+
+				instance.SerializeValue(obj);
+
+				return instance.builder.ToString();
+			}
+
+			void SerializeValue(object value) {
+				IList asList;
+				IDictionary asDict;
+				string asStr;
+
+				if (value == null) {
+					builder.Append("null");
+				} else if ((asStr = value as string) != null) {
+					SerializeString(asStr);
+				} else if (value is bool) {
+					builder.Append((bool) value ? "true" : "false");
+				} else if ((asList = value as IList) != null) {
+					SerializeArray(asList);
+				} else if ((asDict = value as IDictionary) != null) {
+					SerializeObject(asDict);
+				} else if (value is char) {
+					SerializeString(new string((char) value, 1));
+				} else {
+					SerializeOther(value);
+				}
+			}
+
+			void SerializeObject(IDictionary obj) {
+				bool first = true;
+
+				builder.Append('{');
+
+				foreach (object e in obj.Keys) {
+					if (!first) {
+						builder.Append(',');
+					}
+
+					SerializeString(e.ToString());
+					builder.Append(':');
+
+					SerializeValue(obj[e]);
+
+					first = false;
+				}
+
+				builder.Append('}');
+			}
+
+			void SerializeArray(IList anArray) {
+				builder.Append('[');
+
+				bool first = true;
+
+				foreach (object obj in anArray) {
+					if (!first) {
+						builder.Append(',');
+					}
+
+					SerializeValue(obj);
+
+					first = false;
+				}
+
+				builder.Append(']');
+			}
+
+			void SerializeString(string str) {
+				builder.Append('\"');
+
+				char[] charArray = str.ToCharArray();
+				foreach (var c in charArray) {
+					switch (c) {
+						case '"':
+							builder.Append("\\\"");
+							break;
+						case '\\':
+							builder.Append("\\\\");
+							break;
+						case '\b':
+							builder.Append("\\b");
+							break;
+						case '\f':
+							builder.Append("\\f");
+							break;
+						case '\n':
+							builder.Append("\\n");
+							break;
+						case '\r':
+							builder.Append("\\r");
+							break;
+						case '\t':
+							builder.Append("\\t");
+							break;
+						default:
+							int codepoint = Convert.ToInt32(c);
+							if ((codepoint >= 32) && (codepoint <= 126)) {
+								builder.Append(c);
+							} else {
+								builder.Append("\\u");
+								builder.Append(codepoint.ToString("x4"));
+							}
+							break;
+					}
+				}
+
+				builder.Append('\"');
+			}
+
+			void SerializeOther(object value) {
+				// NOTE: decimals lose precision during serialization.
+				// They always have, I'm just letting you know.
+				// Previously floats and doubles lost precision too.
+				if (value is float) {
+					builder.Append(((float) value).ToString("R"));
+				} else if (value is int
+					|| value is uint
+					|| value is long
+					|| value is sbyte
+					|| value is byte
+					|| value is short
+					|| value is ushort
+					|| value is ulong) {
+					builder.Append(value);
+				} else if (value is double
+					|| value is decimal) {
+					builder.Append(Convert.ToDouble(value).ToString("R"));
+				} else {
+					SerializeString(value.ToString());
+				}
+			}
+		}
+	}
+}
+

--- /dev/null
+++ b/KerbalStuffWrapper/Mod.cs
@@ -1,1 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
 
+namespace KerbalStuff
+{
+	public class Mod
+	{
+		public long downloads
+		{
+			get;
+			private set;
+		}
+
+		public string name
+		{
+			get;
+			private set;
+		}
+
+		public long followers
+		{
+			get;
+			private set;
+		}
+
+		public string author
+		{
+			get;
+			private set;
+		}
+
+		public long default_version_id
+		{
+			get;
+			private set;
+		}
+
+		public List<ModVersion> versions
+		{
+			get;
+			private set;
+		}
+
+		public long id
+		{
+			get;
+			private set;
+		}
+
+		public string short_description
+		{
+			get;
+			private set;
+		}
+
+		public string license
+		{
+			get;
+			private set;
+		}
+
+		public Mod(Dictionary<string, object> jsonDict) : this()
+		{
+			this.downloads = (long)jsonDict["downloads"];
+			this.name = (string)jsonDict["name"];
+			this.followers = (long)jsonDict["followers"];
+			this.author = (string)jsonDict["author"];
+			this.default_version_id = (long)jsonDict["default_version_id"];
+			this.id = (long)jsonDict["id"];
+			this.short_description = (string)jsonDict["short_description"];
+
+			if (jsonDict.ContainsKey("versions"))
+			{
+				foreach (var ver in (jsonDict["versions"] as List<object>))
+				{
+					if (ver is Dictionary<string, object>)
+					{
+						this.versions.Add(new ModVersion(ver as Dictionary<string, object>));
+					}
+				}
+			}
+		}
+
+		public Mod(string name, string short_description, string version, string ksp_version, string license) : this()
+		{
+			this.name = name;
+			this.short_description = short_description;
+			this.license = license;
+
+			this.versions.Add(new ModVersion(version, ksp_version));
+		}
+
+		private Mod()
+		{
+			this.versions = new List<ModVersion>();
+		}
+
+		public override string ToString()
+		{
+			return string.Format("Mod: {1}\nid: {6}\nauthor: {3}\ndownloads: {0}\nfollowers: {2}\nshort_description: {7}\ndefault_version_id: {4}\nversions:\n[\n{5}\n]\n", downloads, name, followers, author, default_version_id, string.Join("\n", versions.Select(v => v.ToString()).ToArray()), id, short_description);
+		}
+	}
+
+	public class ModVersion
+	{
+		public string changelog
+		{
+			get;
+			private set;
+		}
+
+		public string ksp_version
+		{
+			get;
+			private set;
+		}
+
+		public string download_path
+		{
+			get;
+			private set;
+		}
+
+		public long id
+		{
+			get;
+			private set;
+		}
+
+		public string friendly_version
+		{
+			get;
+			private set;
+		}
+
+		public ModVersion(Dictionary<string, object> jsonDict) : this()
+		{
+			this.changelog = (string)jsonDict["changelog"];
+			this.ksp_version = (string)jsonDict["ksp_version"];
+			this.download_path = (string)jsonDict["download_path"];
+			this.id = (long)jsonDict["id"];
+			this.friendly_version = (string)jsonDict["friendly_version"];
+		}
+
+		public ModVersion(string version, string ksp_version, string changelog) : this(version, ksp_version)
+		{
+			this.changelog = changelog;
+		}
+
+		public ModVersion(string version, string ksp_version) : this()
+		{
+			this.friendly_version = version;
+			this.ksp_version = ksp_version;
+		}
+
+		private ModVersion() {}
+
+		public override string ToString()
+		{
+			return string.Format("ModVersion {4}:\nid: {3}\nksp_version: {1}\ndownload_path: {2}\nchangelog: {0}", changelog, ksp_version, download_path, id, friendly_version);
+		}
+	}
+}
+
+

--- /dev/null
+++ b/KerbalStuffWrapper/Program.cs
@@ -1,1 +1,57 @@
+using System;
+using System.Collections.Generic;
+using KerbalStuff;
 
+namespace KSWUnitTest
+{
+	using KerbalStuff;
+
+	public static class Program
+	{
+		public static int Main()
+		{
+			/*Console.WriteLine("\n\nSearch:");
+			foreach (Mod obj in KerbalStuff.Search("Quantum Struts"))
+			{
+				Console.WriteLine(obj);
+			}
+
+			Console.WriteLine("\n\nModInfo:");
+			Console.WriteLine(KerbalStuff.ModInfo(123));
+
+			Console.WriteLine("\n\nModLatest:");
+			Console.WriteLine(KerbalStuff.ModLatest(24));
+
+			Console.WriteLine("\n\nUserInfo:");
+			Console.WriteLine(KerbalStuff.UserInfo("toadicus"));
+
+			Console.WriteLine("\n\nUserSearch:");
+			foreach (User user in KerbalStuff.UserSearch("toad"))
+			{
+				Console.WriteLine(user);
+			}*/
+
+			KerbalStuff.Login("toadicus", "redacted");
+
+			var testMod = new Mod(
+				"testMod",
+				"This mod is a test.",
+				"0.1",
+				"0.24.2",
+				"MIT"
+			);
+
+			/*KerbalStuff.Create(
+				testMod,
+				"mod.zip",
+				"mod.zip"
+			);*/
+
+			KerbalStuff.Update(210, new ModVersion("0.3", "0.24.2", "changes changes changes!"), false, "mod.zip", "mod.zip");
+
+			return 0;
+		}
+	}
+}
+
+

--- /dev/null
+++ b/KerbalStuffWrapper/User.cs
@@ -1,1 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
 
+namespace KerbalStuff
+{
+	public class User
+	{
+		public string username
+		{
+			get;
+			private set;
+		}
+
+		public string twitterUsername
+		{
+			get;
+			private set;
+		}
+
+		public List<Mod> mods
+		{
+			get;
+			private set;
+		}
+
+		public string redditUsername
+		{
+			get;
+			private set;
+		}
+
+		public string ircNick
+		{
+			get;
+			private set;
+		}
+
+		public string description
+		{
+			get;
+			private set;
+		}
+
+		public string forumUsername
+		{
+			get;
+			private set;
+		}
+
+		public User(Dictionary<string, object> jsonDict) : this()
+		{
+			this.username = (string)jsonDict["username"];
+			this.twitterUsername = (string)jsonDict["twitterUsername"];
+			this.redditUsername = (string)jsonDict["redditUsername"];
+			this.ircNick = (string)jsonDict["ircNick"];
+			this.forumUsername = (string)jsonDict["forumUsername"];
+
+			this.description = (string)jsonDict["description"];
+
+			this.mods = new List<Mod>();
+
+			foreach (object modObj in (jsonDict["mods"] as List<object>))
+			{
+				this.mods.Add(new Mod(modObj as Dictionary<string, object>));
+			}
+		}
+
+		public User(object jsonObj) : this((Dictionary<string, object>)jsonObj) {}
+
+		private User() {}
+
+		public override string ToString()
+		{
+			return string.Format(
+				"User: username={0}, twitterUsername={1}, redditUsername={3}, ircNick={4}, description={5}, forumUsername={6}\nmods:\n{2}",
+				username,
+				twitterUsername,
+				string.Join(
+					"\n",
+					mods.Select(m => m.ToString()).ToArray()
+				),
+				redditUsername,
+				ircNick,
+				description,
+				forumUsername
+			);
+		}
+	}
+}
+
+