Building a profile
A profile is a choice of tables for each of the four stringprep steps. This page covers the steps one at a time, then puts them together.
Order matters, and PreparationProcessBuilder does not reorder for you — steps run in the order
you add them. RFC 3454 applies mapping, then normalization, then prohibition, then the
bidirectional check. Add them in that order unless you have a reason not to.
Mapping
The mapping step rewrites code points. MappingTable.Build takes any number of dictionary tables
and returns a builder:
var table = MappingTable.Build(Mapping.B1)
.WithValueRangeTable(Prohibited.C12, ' ')
.Compile();
new MappingStep(table).Run("IX"); // "IX"
WithMappingTable adds another dictionary. WithValueRangeTable maps an entire range to one
replacement — useful when a whole block collapses to a single character, as the non-ASCII spaces
in C.1.2 do:
var table = MappingTable.Build(Mapping.B1)
.WithMappingTable(Mapping.B2) // case folding
.WithValueRangeTable(Prohibited.C12, ' ') // every exotic space becomes U+0020
.Compile();
Include merges in another dictionary and, unlike WithMappingTable, does not overwrite entries
that already exist. Remove drops a single code point, and applies to both the dictionary tables
and the value ranges:
var table = MappingTable.Build(Mapping.B1)
.WithValueRangeTable(Prohibited.C12, ' ')
.Remove(0x00AD) // keep the soft hyphen that B.1 would delete
.Remove(0x2000) // and leave one exotic space alone
.Compile();
table.HasReplacement(0x00AD); // false
table.HasReplacement(0x2000); // false
table.HasReplacement(0x2001); // true - still mapped to U+0020
A replacement can be several code points, or none at all. Table B.1 maps its entries to an empty array, which is how "mapped to nothing" is expressed:
table.GetReplacement(0x00AD).Length; // 0 - deleted
table.GetReplacement(0x2000)[0]; // 0x0020
Normalization
There is only one decision here, and for almost every profile it is NFKC:
.WithNormalizationStep() // NFKC
.WithNormalizationStep(NormalizationForm.FormKC) // the same thing, explicit
.WithNormalizationStep(NormalizationForm.FormC) // if a profile demands NFC
This delegates to string.Normalize, so it follows whatever Unicode version the running .NET
release ships. That is worth knowing if you persist prepared strings: a future runtime could
normalize a rare code point differently.
Prohibition
ValueRangeTable.Build takes any number of range tables and merges them. Pass every table your
profile prohibits in one call — merging happens once, at compile time:
var prohibited = ValueRangeTable.Build(
Prohibited.C12, Prohibited.C21, Prohibited.C22, Prohibited.C3, Prohibited.C4,
Prohibited.C5, Prohibited.C6, Prohibited.C7, Prohibited.C8, Prohibited.C9)
.Compile();
You can adjust the set before compiling. Include and IncludeRange add code points; Remove
and RemoveRange take them away:
var prohibited = ValueRangeTable.Build(Prohibited.C21)
.IncludeRange('0', '9') // also reject digits
.Compile();
prohibited.Contains('5'); // true
prohibited.Contains('a'); // false
The compiled table is a sorted array of ranges searched by bisection, so Contains stays cheap no
matter how many tables went into it.
Unassigned code points
RFC 3454 §7 says a profile should reject unassigned code points in stored strings, while queries may tolerate them. Table A.1 is supplied for this and is not prohibited by default:
var stored = ValueRangeTable.Build(Unassigned.A1, Prohibited.C21 /* ... */).Compile();
Note that A.1 reflects the Unicode version RFC 3454 was written against (3.2). Code points unassigned then have since been assigned, so this table rejects characters that are now perfectly valid. Include it only if your profile calls for it.
Bidirectional
The bidirectional check enforces the three rules in RFC 3454 §6: a string containing a RandALCat character may not also contain an LCat character, and if it contains any RandALCat character it must both begin and end with one.
The no-argument overload wires up the standard tables:
.WithBidirectionalStep()
which is equivalent to:
.WithBidirectionalStep(
ValueRangeTable.Create(Prohibited.ChangeDisplayPropertiesOrDeprecated),
ValueRangeTable.Create(BidirectionalTables.RorAL),
ValueRangeTable.Create(BidirectionalTables.L))
The first table is checked before the direction rules and throws ProhibitedValueException; the
default is C.8, the characters that change display properties. The second and third are D.1
(RandALCat) and D.2 (LCat).
Worked profiles
SASLprep — RFC 4013
For usernames and passwords. Maps non-ASCII spaces to U+0020, does not case fold.
var saslprep = PreparationProcess.Build()
.WithMappingStep(MappingTable.Build(Mapping.B1)
.WithValueRangeTable(Prohibited.C12, ' ')
.Compile())
.WithNormalizationStep(NormalizationForm.FormKC)
.WithProhibitedValueStep(ValueRangeTable.Build(
Prohibited.C12, Prohibited.C21, Prohibited.C22, Prohibited.C3, Prohibited.C4,
Prohibited.C5, Prohibited.C6, Prohibited.C7, Prohibited.C8, Prohibited.C9)
.Compile())
.WithBidirectionalStep()
.Compile();
Nameprep — RFC 3491
For internationalized domain labels. Case folds via B.2, and permits ASCII control characters because the surrounding IDNA machinery deals with them.
var nameprep = PreparationProcess.Build()
.WithMappingStep(MappingTable.Create(Mapping.B1, Mapping.B2))
.WithNormalizationStep(NormalizationForm.FormKC)
.WithProhibitedValueStep(ValueRangeTable.Build(
Prohibited.C12, Prohibited.C22, Prohibited.C3, Prohibited.C4, Prohibited.C5,
Prohibited.C6, Prohibited.C7, Prohibited.C8, Prohibited.C9)
.Compile())
.WithBidirectionalStep()
.Compile();
nameprep.Run("ExAmPlE"); // "example"
nameprep.Run("Bücher"); // "bücher"
The differences from SASLprep are the whole of the profile: B.2 instead of the space mapping, and C.2.1 absent from the prohibited set.
Using a step on its own
The steps are public and implement IPreparationProcess, so you can use one without a process
around it — handy for validating input you do not want rewritten:
var check = new ProhibitedValueStep(ValueRangeTable.Create(Prohibited.C21));
check.Run("clean"); // returns the input unchanged, or throws
Every step takes a string and returns a string, which is all IPreparationProcess requires.
Implement it yourself to add a step of your own — a length limit, say — and pass the result
through it alongside the built-in steps.
A note on code points
Every table in RFC 3454 is defined over Unicode code points, not UTF-16 code units, and many
tables contain values above U+FFFF. The steps in this library walk their input by code point, so
a character outside the Basic Multilingual Plane is matched as the single code point it is, and a
well formed surrogate pair is never mistaken for two prohibited surrogates.
If you build tables of your own, give them code point values for the same reason:
var table = ValueRangeTable.Build([0x1F600, 0x1F64F]).Compile(); // emoticons block
table.Contains(0x1F600); // true