first version

This commit is contained in:
RogueAI42 2017-08-29 20:07:02 +10:00
parent 7d637c031c
commit c549608895
10 changed files with 702 additions and 0 deletions

26
README.md Normal file
View file

@ -0,0 +1,26 @@
# Whoa
## About
Whoa is a serialisation library for C#. Its output is ultra small and
you can add members to your type and still be able to deserialise old
versions. Since it stores the bare minimum of type data, it is a bit
more finicky than other serialisation solutions like BinaryFormatter,
but produces much smaller output.
## Usage
For Whoa to produce meaningful output, your type will need to have a
public constructor with no arguments, and every field or property that
you would like to save needs to be public (properties must have a public
getter AND a public setter) and have a Whoa.OrderAttribute. Also, if you
want to preserve backwards compatibility, don't rearrange or remove
any of the members with OrderAttributes. You can, however, add new ones
to the end of your class without issue.
To serialise an object:
`Whoa.Whoa.SerialiseObject(outstream, obj);`
To deserialise an object:
`Whoa.Whoa.DeserialiseObject<T>(instream);`

6
Tests/App.config Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>

View file

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Declan Hoare")]
[assembly: AssemblyProduct("Tests")]
[assembly: AssemblyCopyright("Copyright 2017 Declan Hoare")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("2330458c-1fa0-4a6a-b088-5e8692602acb")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

137
Tests/Test.cs Normal file
View file

@ -0,0 +1,137 @@
// Copyright 2017 Declan Hoare
// This file is part of Whoa.
//
// Whoa is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Whoa is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Whoa. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using Whoa;
namespace Whoa.Tests
{
public static class Test
{
private class Record
{
[Order]
public string title;
[Order]
public string artist;
[Order]
public int rpm;
[Order]
public double price;
[Order]
public bool inLibrary;
[Order]
public bool otherBool;
[Order]
public List<string> songs;
[Order]
public Guid guidForSomeReason;
[Order]
public List<bool> moreBools;
public override string ToString()
{
string ret = $@"{title}
Artist: {artist}
RPM: {rpm}
Price: {price}
";
ret += inLibrary ? "In" : "Not in";
ret += " library" + Environment.NewLine;
ret += "Something else about it: ";
ret += otherBool ? "Yep" : "Nope";
ret += Environment.NewLine;
ret += "Songs:";
ret += Environment.NewLine;
int track = 0;
foreach (string song in songs)
ret += $"{++track}. {song}" + Environment.NewLine;
ret += $"Guid (?!): {guidForSomeReason}";
ret += Environment.NewLine;
ret += "Some more information:";
ret += Environment.NewLine;
foreach (bool info in moreBools)
ret += (info ? "Yes" : "No") + Environment.NewLine;
return ret;
}
}
public static void Main(string[] args)
{
using (var str = new MemoryStream())
{
var rec = new Record()
{
title = "Cool Songs For Cool People",
artist = "Ethan Klein",
rpm = 78,
price = 99.99,
inLibrary = true,
otherBool = true,
songs = new string[]
{
"International Tiles",
"Test Data"
}.ToList(),
guidForSomeReason = Guid.NewGuid(),
moreBools = new bool[] {true, false, false, true, true, true, false, false, true, true}.ToList()
};
string expected = rec.ToString();
Whoa.SerialiseObject(str, rec);
if (args.Length > 0) // Save output for debugging.
{
str.Position = 0;
using (var fobj = File.OpenWrite(args[0]))
str.CopyTo(fobj);
}
str.Position = 0;
var res = Whoa.DeserialiseObject<Record>(str);
string actual = res.ToString();
Console.Write("Test ");
if (expected == actual)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("passed");
Console.ResetColor();
Console.Write(actual);
Environment.Exit(0);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("FAILED");
Console.ResetColor();
Console.WriteLine("Expected:");
Console.Write(expected);
Console.WriteLine("");
Console.WriteLine("Actual:");
Console.Write(actual);
Environment.Exit(1);
}
}
}
}
}

58
Tests/Tests.csproj Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2330458C-1FA0-4A6A-B088-5E8692602ACB}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>Tests</RootNamespace>
<AssemblyName>Tests</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Test.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Whoa\Whoa.csproj">
<Project>{493a0902-9d25-4c60-a419-2c4082577c9d}</Project>
<Name>Whoa</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

28
Whoa.sln Normal file
View file

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26430.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Whoa", "Whoa\Whoa.csproj", "{493A0902-9D25-4C60-A419-2C4082577C9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{2330458C-1FA0-4A6A-B088-5E8692602ACB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{493A0902-9D25-4C60-A419-2C4082577C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{493A0902-9D25-4C60-A419-2C4082577C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{493A0902-9D25-4C60-A419-2C4082577C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{493A0902-9D25-4C60-A419-2C4082577C9D}.Release|Any CPU.Build.0 = Release|Any CPU
{2330458C-1FA0-4A6A-B088-5E8692602ACB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2330458C-1FA0-4A6A-B088-5E8692602ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2330458C-1FA0-4A6A-B088-5E8692602ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2330458C-1FA0-4A6A-B088-5E8692602ACB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

31
Whoa/OrderAttribute.cs Normal file
View file

@ -0,0 +1,31 @@
// Copyright 2017 Declan Hoare
// This file is part of Whoa.
//
// Whoa is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Whoa is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Whoa. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Runtime.CompilerServices;
namespace Whoa
{
public sealed class OrderAttribute : Attribute
{
private readonly int order_;
public OrderAttribute([CallerLineNumber]int order = 0)
{
order_ = order;
}
public int Order { get { return order_; } }
}
}

View file

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Whoa")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Declan Hoare")]
[assembly: AssemblyProduct("Whoa")]
[assembly: AssemblyCopyright("Copyright 2017 Declan Hoare")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("493a0902-9d25-4c60-a419-2c4082577c9d")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

296
Whoa/Whoa.cs Normal file
View file

@ -0,0 +1,296 @@
// Copyright 2017 Declan Hoare
// This file is part of Whoa.
//
// Whoa is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Whoa is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Whoa. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Reflection;
namespace Whoa
{
public static class Whoa
{
private class SpecialSerialiserAttribute: Attribute
{
public Type t { get; private set; }
public SpecialSerialiserAttribute(Type in_t)
{
t = in_t;
}
}
private class SpecialDeserialiserAttribute: Attribute
{
public Type t { get; private set; }
public SpecialDeserialiserAttribute(Type in_t)
{
t = in_t;
}
}
[SpecialSerialiser(typeof(Guid))]
private static void SerialiseGuid(Stream fobj, dynamic obj)
{
fobj.Write(obj.ToByteArray(), 0, 16);
}
[SpecialDeserialiser(typeof(Guid))]
private static object DeserialiseGuid(Stream fobj)
{
var guid = new byte[16];
fobj.Read(guid, 0, 16);
return new Guid(guid);
}
private static IOrderedEnumerable<MemberInfo> Members(Type t)
{
return t.GetProperties().Select(m => m as MemberInfo).Concat(t.GetFields().Select(m => m as MemberInfo)).Where(m => (m.GetCustomAttributes(typeof(OrderAttribute), false).SingleOrDefault() as OrderAttribute) != null).OrderBy(m => (m.GetCustomAttributes(typeof(OrderAttribute), false).SingleOrDefault() as OrderAttribute).Order);
}
private static List<bool> ReadBitfield(Stream fobj, int count)
{
sbyte bit = 7;
int cur = 0;
var bitfields = new byte[count / 8 + 1];
var bools = new List<bool>(count);
fobj.Read(bitfields, 0, bitfields.Length);
for (int i = 0; i < count; i++)
{
bools.Add(((bitfields[cur] >> bit) & 1) == 1);
bit--;
if (bit < 0)
{
bit = 7;
cur++;
}
}
return bools;
}
private static void WriteBitfield(Stream fobj, IEnumerable<bool> bools)
{
sbyte bit = 7;
int cur = 0;
var bitfields = new byte[bools.Count() / 8 + 1];
foreach (bool mybool in bools)
{
if (mybool)
bitfields[cur] |= (byte) (1 << bit);
bit--;
if (bit < 0)
{
bit = 7;
cur++;
}
}
fobj.Write(bitfields, 0, bitfields.Length);
}
public static T DeserialiseObject<T>(Stream fobj)
{
return (T)DeserialiseObject(typeof(T), fobj);
}
public static object DeserialiseObject(Type t, Stream fobj)
{
using (var read = new BinaryReader(fobj, Encoding.UTF8, true))
{
if (t.IsGenericType)
{
var gent = t.GetGenericTypeDefinition();
if (gent == typeof(List<>))
{
int numelems = read.ReadInt32();
dynamic retl = Activator.CreateInstance(t, new object[] { numelems });
Type elemtype = t.GetGenericArguments()[0];
for (int i = 0; i < numelems; i++)
retl.Add((dynamic)DeserialiseObject(elemtype, fobj));
return retl;
}
if (gent == typeof(Dictionary<,>))
{
int numpairs = read.ReadInt32();
dynamic retd = Activator.CreateInstance(t, new object[] { numpairs });
Type[] arguments = t.GetGenericArguments();
for (int i = 0; i < numpairs; i++)
{
dynamic key = DeserialiseObject(arguments[0], fobj);
dynamic val = DeserialiseObject(arguments[1], fobj);
retd.Add(key, val);
}
return retd;
}
}
// A little self reflection.
var specialmethod = typeof(Whoa).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).FirstOrDefault(m =>
{
var attr = m.GetCustomAttributes(typeof(SpecialDeserialiserAttribute), false).SingleOrDefault() as SpecialDeserialiserAttribute;
if (attr == null)
return false;
return attr.t == t;
});
if (specialmethod != null)
return specialmethod.Invoke(null, new object[] { fobj });
var readermethod = typeof(BinaryReader).GetMethods().FirstOrDefault(m => m.Name.Length > 4 && m.Name.StartsWith("Read") && m.ReturnType == t);
if (readermethod != null)
return readermethod.Invoke(read, new object[] { });
object ret = t.GetConstructor(Type.EmptyTypes).Invoke(new object[] { });
int nummembers = read.ReadInt32();
var bools = new List<dynamic>();
foreach (dynamic member in Members(t).Take(nummembers))
{
Type memt;
if (member.MemberType == MemberTypes.Field)
memt = member.FieldType;
else
memt = member.PropertyType;
if (memt == typeof(bool))
bools.Add(member);
else if (memt == typeof(List<bool>))
member.SetValue(ret, ReadBitfield(fobj, read.ReadInt32()));
else if (memt == typeof(bool[]))
member.SetValue(ret, ReadBitfield(fobj, read.ReadInt32()).ToArray());
else
member.SetValue(ret, DeserialiseObject(memt, fobj));
}
if (bools.Count > 0)
{
var loaded = ReadBitfield(fobj, bools.Count);
foreach (var item in bools.Zip(loaded, (m, b) => new { Member = m, Value = b }))
item.Member.SetValue(ret, item.Value);
}
return ret;
}
}
public static void SerialiseObject(Stream fobj, dynamic obj)
{
using (var write = new BinaryWriter(fobj, Encoding.UTF8, true))
{
Type t = obj.GetType();
if (t.IsGenericType)
{
var gent = t.GetGenericTypeDefinition();
if (gent == typeof(List<>))
{
if (obj == null)
{
write.Write(0);
return;
}
write.Write(obj.Count);
foreach (dynamic item in obj)
SerialiseObject(fobj, item);
return;
}
if (gent == typeof(Dictionary<,>))
{
if (obj == null)
{
write.Write(0);
return;
}
write.Write(obj.Count);
foreach (dynamic pair in obj)
{
SerialiseObject(fobj, pair.Key);
SerialiseObject(fobj, pair.Value);
}
return;
}
}
var specialmethod = typeof(Whoa).GetMethods(BindingFlags.Static | BindingFlags.NonPublic).FirstOrDefault(m =>
{
var attr = m.GetCustomAttributes(typeof(SpecialSerialiserAttribute), false).SingleOrDefault() as SpecialSerialiserAttribute;
if (attr == null)
return false;
return attr.t == t;
});
if (specialmethod != null)
{
specialmethod.Invoke(null, new object[] { fobj, obj });
return;
}
try
{
write.Write(obj); // Will fail if not an integral type
return;
}
catch
{
}
if (obj == null)
{
write.Write(0);
return;
}
var bools = new List<bool>();
var members = Members(t);
write.Write(members.Count());
foreach (dynamic member in members)
{
Type memt;
if (member.MemberType == MemberTypes.Field)
memt = member.FieldType;
else
memt = member.PropertyType;
if (memt == typeof(bool))
bools.Add(member.GetValue(obj));
else if (typeof(IEnumerable<bool>).IsAssignableFrom(memt))
{
var val = member.GetValue(obj) as IEnumerable<bool>;
if (val == null)
write.Write(0);
else
{
write.Write(val.Count());
WriteBitfield(fobj, val.ToList());
}
}
else
SerialiseObject(fobj, member.GetValue(obj));
}
WriteBitfield(fobj, bools);
}
}
}
}

48
Whoa/Whoa.csproj Normal file
View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{493A0902-9D25-4C60-A419-2C4082577C9D}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Whoa</RootNamespace>
<AssemblyName>Whoa</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="OrderAttribute.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Whoa.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>