2017-04-07 09:09:57 +00:00
|
|
|
// Copyright 2015 The Go Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package precis
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
"golang.org/x/text/runes"
|
|
|
|
"golang.org/x/text/secure/bidirule"
|
|
|
|
"golang.org/x/text/transform"
|
|
|
|
"golang.org/x/text/width"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
errDisallowedRune = errors.New("precis: disallowed rune encountered")
|
|
|
|
)
|
|
|
|
|
|
|
|
var dpTrie = newDerivedPropertiesTrie(0)
|
|
|
|
|
|
|
|
// A Profile represents a set of rules for normalizing and validating strings in
|
|
|
|
// the PRECIS framework.
|
|
|
|
type Profile struct {
|
|
|
|
options
|
|
|
|
class *class
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewIdentifier creates a new PRECIS profile based on the Identifier string
|
|
|
|
// class. Profiles created from this class are suitable for use where safety is
|
|
|
|
// prioritized over expressiveness like network identifiers, user accounts, chat
|
|
|
|
// rooms, and file names.
|
|
|
|
func NewIdentifier(opts ...Option) *Profile {
|
|
|
|
return &Profile{
|
|
|
|
options: getOpts(opts...),
|
|
|
|
class: identifier,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFreeform creates a new PRECIS profile based on the Freeform string class.
|
|
|
|
// Profiles created from this class are suitable for use where expressiveness is
|
|
|
|
// prioritized over safety like passwords, and display-elements such as
|
|
|
|
// nicknames in a chat room.
|
|
|
|
func NewFreeform(opts ...Option) *Profile {
|
|
|
|
return &Profile{
|
|
|
|
options: getOpts(opts...),
|
|
|
|
class: freeform,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewTransformer creates a new transform.Transformer that performs the PRECIS
|
|
|
|
// preparation and enforcement steps on the given UTF-8 encoded bytes.
|
|
|
|
func (p *Profile) NewTransformer() *Transformer {
|
|
|
|
var ts []transform.Transformer
|
|
|
|
|
|
|
|
// These transforms are applied in the order defined in
|
|
|
|
// https://tools.ietf.org/html/rfc7564#section-7
|
|
|
|
|
|
|
|
if p.options.foldWidth {
|
|
|
|
ts = append(ts, width.Fold)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, f := range p.options.additional {
|
|
|
|
ts = append(ts, f())
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.options.cases != nil {
|
|
|
|
ts = append(ts, p.options.cases)
|
|
|
|
}
|
|
|
|
|
|
|
|
ts = append(ts, p.options.norm)
|
|
|
|
|
|
|
|
if p.options.bidiRule {
|
|
|
|
ts = append(ts, bidirule.New())
|
|
|
|
}
|
|
|
|
|
|
|
|
ts = append(ts, &checker{p: p, allowed: p.Allowed()})
|
|
|
|
|
|
|
|
// TODO: Add the disallow empty rule with a dummy transformer?
|
|
|
|
|
|
|
|
return &Transformer{transform.Chain(ts...)}
|
|
|
|
}
|
|
|
|
|
|
|
|
var errEmptyString = errors.New("precis: transformation resulted in empty string")
|
|
|
|
|
|
|
|
type buffers struct {
|
|
|
|
src []byte
|
|
|
|
buf [2][]byte
|
|
|
|
next int
|
|
|
|
}
|
|
|
|
|
2017-04-07 10:49:53 +00:00
|
|
|
func (b *buffers) init(n int) {
|
|
|
|
b.buf[0] = make([]byte, 0, n)
|
|
|
|
b.buf[1] = make([]byte, 0, n)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *buffers) apply(t transform.Transformer) (err error) {
|
|
|
|
// TODO: use Span, once available.
|
2017-04-07 09:09:57 +00:00
|
|
|
x := b.next & 1
|
2017-04-07 10:49:53 +00:00
|
|
|
b.src, _, err = transform.Append(t, b.buf[x][:0], b.src)
|
2017-04-07 09:09:57 +00:00
|
|
|
b.buf[x] = b.src
|
|
|
|
b.next++
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-04-07 10:49:53 +00:00
|
|
|
func (b *buffers) enforce(p *Profile, src []byte) (str []byte, err error) {
|
2017-04-07 09:09:57 +00:00
|
|
|
b.src = src
|
|
|
|
|
|
|
|
// These transforms are applied in the order defined in
|
|
|
|
// https://tools.ietf.org/html/rfc7564#section-7
|
|
|
|
|
|
|
|
// TODO: allow different width transforms options.
|
2017-04-07 10:49:53 +00:00
|
|
|
if p.options.foldWidth {
|
|
|
|
// TODO: use Span, once available.
|
|
|
|
if err = b.apply(width.Fold); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-04-07 09:09:57 +00:00
|
|
|
}
|
|
|
|
for _, f := range p.options.additional {
|
|
|
|
if err = b.apply(f()); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if p.options.cases != nil {
|
2017-04-07 10:49:53 +00:00
|
|
|
if err = b.apply(p.options.cases); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-04-07 09:09:57 +00:00
|
|
|
}
|
2017-04-07 10:49:53 +00:00
|
|
|
if n := p.norm.QuickSpan(b.src); n < len(b.src) {
|
|
|
|
x := b.next & 1
|
|
|
|
n = copy(b.buf[x], b.src[:n])
|
|
|
|
b.src, _, err = transform.Append(p.norm, b.buf[x][:n], b.src[n:])
|
|
|
|
b.buf[x] = b.src
|
|
|
|
b.next++
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-04-07 09:09:57 +00:00
|
|
|
}
|
2017-04-07 10:49:53 +00:00
|
|
|
if p.options.bidiRule {
|
|
|
|
if err := b.apply(bidirule.New()); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-04-07 09:09:57 +00:00
|
|
|
}
|
|
|
|
c := checker{p: p}
|
|
|
|
if _, err := c.span(b.src, true); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if p.disallow != nil {
|
|
|
|
for i := 0; i < len(b.src); {
|
|
|
|
r, size := utf8.DecodeRune(b.src[i:])
|
|
|
|
if p.disallow.Contains(r) {
|
|
|
|
return nil, errDisallowedRune
|
|
|
|
}
|
|
|
|
i += size
|
|
|
|
}
|
|
|
|
}
|
2017-04-07 10:49:53 +00:00
|
|
|
|
|
|
|
// TODO: Add the disallow empty rule with a dummy transformer?
|
|
|
|
|
2017-04-07 09:09:57 +00:00
|
|
|
if p.options.disallowEmpty && len(b.src) == 0 {
|
|
|
|
return nil, errEmptyString
|
|
|
|
}
|
|
|
|
return b.src, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append appends the result of applying p to src writing the result to dst.
|
|
|
|
// It returns an error if the input string is invalid.
|
|
|
|
func (p *Profile) Append(dst, src []byte) ([]byte, error) {
|
|
|
|
var buf buffers
|
2017-04-07 10:49:53 +00:00
|
|
|
buf.init(8 + len(src) + len(src)>>2)
|
|
|
|
b, err := buf.enforce(p, src)
|
2017-04-07 09:09:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return append(dst, b...), nil
|
|
|
|
}
|
|
|
|
|
2017-04-07 10:49:53 +00:00
|
|
|
// Bytes returns a new byte slice with the result of applying the profile to b.
|
|
|
|
func (p *Profile) Bytes(b []byte) ([]byte, error) {
|
2017-04-07 09:09:57 +00:00
|
|
|
var buf buffers
|
2017-04-07 10:49:53 +00:00
|
|
|
buf.init(8 + len(b) + len(b)>>2)
|
|
|
|
b, err := buf.enforce(p, b)
|
2017-04-07 09:09:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if buf.next == 0 {
|
|
|
|
c := make([]byte, len(b))
|
|
|
|
copy(c, b)
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
2017-04-07 10:49:53 +00:00
|
|
|
// String returns a string with the result of applying the profile to s.
|
|
|
|
func (p *Profile) String(s string) (string, error) {
|
2017-04-07 09:09:57 +00:00
|
|
|
var buf buffers
|
2017-04-07 10:49:53 +00:00
|
|
|
buf.init(8 + len(s) + len(s)>>2)
|
|
|
|
b, err := buf.enforce(p, []byte(s))
|
2017-04-07 09:09:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return string(b), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compare enforces both strings, and then compares them for bit-string identity
|
|
|
|
// (byte-for-byte equality). If either string cannot be enforced, the comparison
|
|
|
|
// is false.
|
|
|
|
func (p *Profile) Compare(a, b string) bool {
|
2017-04-07 10:49:53 +00:00
|
|
|
a, err := p.String(a)
|
2017-04-07 09:09:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
2017-04-07 10:49:53 +00:00
|
|
|
b, err = p.String(b)
|
2017-04-07 09:09:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-04-07 10:49:53 +00:00
|
|
|
// TODO: This is out of order. Need to extract the transformation logic and
|
|
|
|
// put this in where the normal case folding would go (but only for
|
|
|
|
// comparison).
|
|
|
|
if p.options.ignorecase {
|
|
|
|
a = width.Fold.String(a)
|
|
|
|
b = width.Fold.String(a)
|
|
|
|
}
|
|
|
|
|
|
|
|
return a == b
|
2017-04-07 09:09:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Allowed returns a runes.Set containing every rune that is a member of the
|
|
|
|
// underlying profile's string class and not disallowed by any profile specific
|
|
|
|
// rules.
|
|
|
|
func (p *Profile) Allowed() runes.Set {
|
|
|
|
if p.options.disallow != nil {
|
|
|
|
return runes.Predicate(func(r rune) bool {
|
|
|
|
return p.class.Contains(r) && !p.options.disallow.Contains(r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return p.class
|
|
|
|
}
|
|
|
|
|
|
|
|
type checker struct {
|
|
|
|
p *Profile
|
|
|
|
allowed runes.Set
|
|
|
|
|
|
|
|
beforeBits catBitmap
|
|
|
|
termBits catBitmap
|
|
|
|
acceptBits catBitmap
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *checker) Reset() {
|
|
|
|
c.beforeBits = 0
|
|
|
|
c.termBits = 0
|
|
|
|
c.acceptBits = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *checker) span(src []byte, atEOF bool) (n int, err error) {
|
|
|
|
for n < len(src) {
|
|
|
|
e, sz := dpTrie.lookup(src[n:])
|
|
|
|
d := categoryTransitions[category(e&catMask)]
|
|
|
|
if sz == 0 {
|
|
|
|
if !atEOF {
|
|
|
|
return n, transform.ErrShortSrc
|
|
|
|
}
|
|
|
|
return n, errDisallowedRune
|
|
|
|
}
|
|
|
|
if property(e) < c.p.class.validFrom {
|
|
|
|
if d.rule == nil {
|
|
|
|
return n, errDisallowedRune
|
|
|
|
}
|
|
|
|
doLookAhead, err := d.rule(c.beforeBits)
|
|
|
|
if err != nil {
|
|
|
|
return n, err
|
|
|
|
}
|
|
|
|
if doLookAhead {
|
|
|
|
c.beforeBits &= d.keep
|
|
|
|
c.beforeBits |= d.set
|
|
|
|
// We may still have a lookahead rule which we will require to
|
|
|
|
// complete (by checking termBits == 0) before setting the new
|
|
|
|
// bits.
|
|
|
|
if c.termBits != 0 && (!c.checkLookahead() || c.termBits == 0) {
|
|
|
|
return n, err
|
|
|
|
}
|
|
|
|
c.termBits = d.term
|
|
|
|
c.acceptBits = d.accept
|
|
|
|
n += sz
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
c.beforeBits &= d.keep
|
|
|
|
c.beforeBits |= d.set
|
|
|
|
if c.termBits != 0 && !c.checkLookahead() {
|
|
|
|
return n, errContext
|
|
|
|
}
|
|
|
|
n += sz
|
|
|
|
}
|
|
|
|
if m := c.beforeBits >> finalShift; c.beforeBits&m != m || c.termBits != 0 {
|
|
|
|
err = errContext
|
|
|
|
}
|
|
|
|
return n, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *checker) checkLookahead() bool {
|
|
|
|
switch {
|
|
|
|
case c.beforeBits&c.termBits != 0:
|
|
|
|
c.termBits = 0
|
|
|
|
c.acceptBits = 0
|
|
|
|
case c.beforeBits&c.acceptBits != 0:
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: we may get rid of this transform if transform.Chain understands
|
|
|
|
// something like a Spanner interface.
|
|
|
|
func (c checker) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
|
|
|
short := false
|
|
|
|
if len(dst) < len(src) {
|
|
|
|
src = src[:len(dst)]
|
|
|
|
atEOF = false
|
|
|
|
short = true
|
|
|
|
}
|
|
|
|
nSrc, err = c.span(src, atEOF)
|
|
|
|
nDst = copy(dst, src[:nSrc])
|
|
|
|
if short && (err == transform.ErrShortSrc || err == nil) {
|
|
|
|
err = transform.ErrShortDst
|
|
|
|
}
|
|
|
|
return nDst, nSrc, err
|
|
|
|
}
|