Skip to content

Instantly share code, notes, and snippets.

@jedahu
Last active August 29, 2015 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jedahu/9881337 to your computer and use it in GitHub Desktop.
Save jedahu/9881337 to your computer and use it in GitHub Desktop.
Safe strings
using System.Diagnostics.Contracts;
[ContractClass(typeof(ILanguageContract<>))]
public interface ILanguage<A>
{
A Literal(string s);
string Escape(string s);
string Render(A a);
}
[ContractClassFor(typeof(ILanguage<>))]
public abstract class ILanguageContract<A>
: ILanguage<A>
{
A ILanguage<A>.Literal(string s)
{
Contract.Requires(Pred.NotNull(s));
Contract.Ensures(Pred.NotNull(Contract.Result<A>()));
throw new System.NotImplementedException();
}
string ILanguage<A>.Escape(string s)
{
Contract.Requires(Pred.NotNull(s));
Contract.Ensures(Pred.NotNull(Contract.Result<string>()));
throw new System.NotImplementedException();
}
string ILanguage<A>.Render(A a)
{
Contract.Requires(Pred.NotNull(a));
Contract.Ensures(Pred.NotNull(Contract.Result<string>()));
throw new System.NotImplementedException();
}
}
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
[Pure]
public sealed class SafeString<A>
{
private readonly ILanguage<A> lang;
private readonly Fragment frag;
private SafeString(ILanguage<A> lang, Fragment frag)
{
Contract.Requires(Pred.NotNull(lang));
Contract.Requires(Pred.NotNull(frag));
Contract.Requires(
typeof (A).IsDefined(typeof (PureAttribute), false),
"SafeString must be used with an immutable type. "
+ "Make sure the type is immutable, then add the "
+ "System.Diagnostics.Contracts.PureAttribute to its class.");
Contract.Requires(
typeof (A).GetConstructors().Length == 0,
"SafeString must be used with a type without public constructors.");
this.lang = lang;
this.frag = frag;
}
internal static SafeString<A> Literal(ILanguage<A> lang, string s)
{
return new SafeString<A>(lang, new Single(lang.Literal(s)));
}
internal static SafeString<A> Escape(ILanguage<A> lang, string s)
{
return Literal(lang, lang.Escape(s));
}
public void Render(TextWriter w)
{
foreach (var s in frag.Render(lang)) w.Write(s);
}
public string Render()
{
return string.Concat(frag.Render(lang));
}
public SafeString<A> Append(SafeString<A> ss)
{
return new SafeString<A>(lang, new Concat(frag, ss.frag));
}
public SafeString<A> Lit(string s)
{
return new SafeString<A>(
lang,
new Concat(frag, new Single(lang.Literal(s))));
}
public SafeString<A> Esc(string s)
{
return Lit(lang.Escape(s));
}
private interface Fragment
{
IEnumerable<string> Render(ILanguage<A> lang);
}
private struct Single : Fragment
{
private readonly A a;
public Single(A a)
{
this.a = a;
}
public IEnumerable<string> Render(ILanguage<A> lang)
{
yield return lang.Render(a);
}
}
private struct Concat : Fragment
{
private readonly Fragment frag1;
private readonly Fragment frag2;
public Concat(Fragment frag1, Fragment frag2)
{
this.frag1 = frag1;
this.frag2 = frag2;
}
public IEnumerable<string> Render(ILanguage<A> lang)
{
return frag1.Render(lang).Concat(frag2.Render(lang));
}
}
}
public static class SafeString
{
public static SafeString<A> Literal<A>(ILanguage<A> lang, string s)
{
return SafeString<A>.Literal(lang, s);
}
public static SafeString<A> Escape<A>(ILanguage<A> lang, string s)
{
return SafeString<A>.Escape(lang, s);
}
}
using FsCheck.Xunit;
public abstract class LanguageLaws<T>
{
protected abstract ILanguage<T> Language { get; }
[Property]
public bool Literal_Render_is_identity(string s)
{
return s == Language.Render(Language.Literal(s));
}
}
using FsCheck.Xunit;
public abstract class SafeStringLaws<A>
{
static SafeStringLaws()
{
TestSetup.Init();
}
protected abstract SafeString<A> Literal(string s);
protected abstract SafeString<A> Escape(string s);
protected abstract ILanguage<A> Language { get; }
[Property]
public bool Literal_Render_is_identity(string s)
{
return s == Literal(s).Render();
}
[Property]
public bool Escape_Render_is_escape(string s)
{
return Language.Escape(s) == Escape(s).Render();
}
[Property]
public bool
Append_distributes_over_string_concatenation(
string s1, string s2)
{
return
s1 + s2 == Literal(s1).Append(Literal(s2)).Render()
&& Language.Escape(s1 + s2) == Escape(s1).Append(Escape(s2)).Render()
&& s1 + Language.Escape(s2) == Literal(s1).Append(Escape(s2)).Render()
&& Language.Escape(s1) + s2.Value == Escape(s1).Append(Literal(s2)).Render();
}
}
using System.Diagnostics.Contracts;
using system.Net;
[Pure]
public sealed class Html
{
private readonly string htmlString;
private Html(string s)
{
htmlString = s;
}
public static SafeString<Html> Literal(string s)
{
return SafeString.Literal(Language, s);
}
public static SafeString<Html> Escape(string s)
{
return SafeString.Escape(Language, s);
}
public static readonly ILanguage<Html> Language = new HtmlLanguage();
private struct HtmlLanguage
: ILanguage<Html>
{
Html ILanguage<Html>.Literal(string s)
{
return new Html(s);
}
public string Escape(string s)
{
return WebUtility.HtmlEncode(s);
}
public string Render(Html a)
{
return a.htmlString;
}
}
}
// The only way to construct a SafeString<Html> is using one of Html.Literal() or Html.Escape().
// The only way to append strings is using .Lit() or .Esc().
SafeString<Html> msg = Html.Escape("A Dangerous < message.");
SafeString<Html> html = Html.Literal("<message>").Esc("Hello & read ").Append(msg).Lit("</message>");
var tw = new TextWriter();
html.Render(tw);
// Using the type-system to our advantage:
public void MethodWithEscapedHtmlStringPrecondition(SafeString<Html> str)
{
// Assuming the caller has used Literal(), Escape(), Lit(), and Esc() judiciously,
// at this point we know we have a correctly escaped HTML string. A stronger
// guarantee than if the argument was a string.
var htmlString = html.Render();
...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment