snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
434
server/channels/app/password/phcparser/parser.go
Normal file
434
server/channels/app/password/phcparser/parser.go
Normal file
@@ -0,0 +1,434 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Package parser provides a type to parse strings conformant to the PHC string format:
|
||||
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
package phcparser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// PHC represents a PHC string, with all its parts already parsed:
|
||||
type PHC struct {
|
||||
// Id is the identifier of the hashing function.
|
||||
Id string
|
||||
|
||||
// Version is an optional string containing the specific version of the
|
||||
// hashing function used.
|
||||
Version string
|
||||
|
||||
// Params is a map of parameters, containing a set of all parameter names
|
||||
// with their corresponding values.
|
||||
Params map[string]string
|
||||
|
||||
// Salt is the base64-encoded salt used when hashing the original password.
|
||||
Salt string
|
||||
|
||||
// Hash is the base64-encoded hash generated when hashing the original
|
||||
// password with the function specified by all other parameters.
|
||||
Hash string
|
||||
}
|
||||
|
||||
// Parser is a wrapper of a limited bufio.Reader that will parse its input into
|
||||
// a [PHC].
|
||||
type Parser struct {
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// MaxRunes is the maximum number of runes allowed in a PHC string. If the
|
||||
// string is longer, the remaining runes are ignored.
|
||||
const MaxRunes = 256
|
||||
|
||||
// New builds a new [Parser], limiting the input to [MaxRunes] runes.
|
||||
func New(r io.Reader) *Parser {
|
||||
return &Parser{reader: bufio.NewReader(io.LimitReader(r, MaxRunes))}
|
||||
}
|
||||
|
||||
// Token represents a minimal unit of meaning in the parsed string.
|
||||
type Token uint
|
||||
|
||||
const (
|
||||
// ILLEGAL is a token representing an illegal token
|
||||
ILLEGAL Token = 1 << iota
|
||||
|
||||
// Separator tokens
|
||||
// EOF is a token representing the end of the input
|
||||
EOF
|
||||
// DOLLARSIGN is a token representing a '$'
|
||||
DOLLARSIGN
|
||||
// COMMA is a token representing a ','
|
||||
COMMA
|
||||
// EQUALSIGN is a token representing a '='
|
||||
EQUALSIGN
|
||||
|
||||
// Literals
|
||||
// FUNCTIONID is a token representing a non-empty set of any of the following symbols:
|
||||
// [a-z0-9-]
|
||||
FUNCTIONID
|
||||
// PARAMNAME is a token representing a non-empty set of any of the following symbols:
|
||||
// [a-z0-9-]
|
||||
PARAMNAME
|
||||
// PARAMVALUE is a token representing a non-empty set of any of the following symbols:
|
||||
// [a-zA-Z0-9/+.-]
|
||||
PARAMVALUE
|
||||
// B64ENCODED is a token representing a non-empty set of any of the following symbols:
|
||||
// [A-Za-z0-9+/]
|
||||
B64ENCODED
|
||||
)
|
||||
|
||||
const (
|
||||
// IDENT is a generic identifier that represents any of its possibilities:
|
||||
// either a FUNCTIONID, a PARAMNAME, a PARAMVALUE or a B64ENCODED
|
||||
IDENT Token = FUNCTIONID | PARAMNAME | PARAMVALUE | B64ENCODED
|
||||
)
|
||||
|
||||
// eof is a constant literal representing EOF
|
||||
const eof = rune(0)
|
||||
|
||||
// [a-z]
|
||||
func isLowercaseLetter(ch rune) bool {
|
||||
return (ch >= 'a' && ch <= 'z')
|
||||
}
|
||||
|
||||
// [A-Za-z]
|
||||
func isLetter(ch rune) bool {
|
||||
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')
|
||||
}
|
||||
|
||||
// [0-9]
|
||||
func isDigit(ch rune) bool {
|
||||
return (ch >= '0' && ch <= '9')
|
||||
}
|
||||
|
||||
// [A-Za-z0-9+/]
|
||||
func isB64(ch rune) bool {
|
||||
return isLetter(ch) || isDigit(ch) || ch == '+' || ch == '/'
|
||||
}
|
||||
|
||||
// [/+.-]
|
||||
func isSymbol(ch rune) bool {
|
||||
return ch == '/' || ch == '+' || ch == '.' || ch == '-'
|
||||
}
|
||||
|
||||
// [a-z0-9-]
|
||||
func isLowercaseLetterOrDigitOrMinus(ch rune) bool {
|
||||
return isLowercaseLetter(ch) || isDigit(ch) || ch == '-'
|
||||
}
|
||||
|
||||
// [a-zA-Z0-9/+.-]
|
||||
func isLetterOrDigitOrSymbol(ch rune) bool {
|
||||
return isLetter(ch) || isDigit(ch) || isSymbol(ch)
|
||||
}
|
||||
|
||||
// no identifiers allowed
|
||||
func none(ch rune) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// read reads a single rune, returning [eof] in case of any error.
|
||||
func (p *Parser) read() rune {
|
||||
ch, _, err := p.reader.ReadRune()
|
||||
if err != nil {
|
||||
return eof
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// unread unreads a single rune
|
||||
func (p *Parser) unread() { _ = p.reader.UnreadRune() }
|
||||
|
||||
// scan scans either an identifier whose runes are allowed by the provided
|
||||
// function, or a single separator token: EOF $ , =
|
||||
func (p *Parser) scan(isIdentAllowedRune func(rune) bool) (tok Token, lit string) {
|
||||
ch := p.read()
|
||||
|
||||
if isIdentAllowedRune(ch) {
|
||||
p.unread()
|
||||
return p.scanIdent(isIdentAllowedRune)
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case eof:
|
||||
return EOF, ""
|
||||
case '$':
|
||||
return DOLLARSIGN, string(ch)
|
||||
case ',':
|
||||
return COMMA, string(ch)
|
||||
case '=':
|
||||
return EQUALSIGN, string(ch)
|
||||
}
|
||||
|
||||
return ILLEGAL, string(ch)
|
||||
}
|
||||
|
||||
// scanIdent scans a series of contiguous runes allowed by the provided function
|
||||
// that form a single identifier.
|
||||
func (p *Parser) scanIdent(isIdentAllowedRune func(rune) bool) (tok Token, lit string) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteRune(p.read())
|
||||
|
||||
for {
|
||||
ch := p.read()
|
||||
if ch == eof {
|
||||
break
|
||||
}
|
||||
|
||||
if !isIdentAllowedRune(ch) {
|
||||
p.unread()
|
||||
break
|
||||
}
|
||||
|
||||
_, _ = buf.WriteRune(ch)
|
||||
}
|
||||
|
||||
// On success, return the generic IDENT, the check for each specific
|
||||
// identifier is already done with isIdentAllowedRune
|
||||
return IDENT, buf.String()
|
||||
}
|
||||
|
||||
// scanSeparator scans one of the separator tokens:
|
||||
// - EOF
|
||||
// - $
|
||||
// - ,
|
||||
// - =
|
||||
func (p *Parser) scanSeparator() (tok Token, lit string) {
|
||||
return p.scan(none)
|
||||
}
|
||||
|
||||
// parseToken returns the literal string of an expected token, or an error.
|
||||
// expected can be an ORed expression of different tokens, like
|
||||
//
|
||||
// EOF | DOLLARSIGN | FUNCTIONID
|
||||
//
|
||||
// In this case, any of those tokens are allowd, and its literal will be returned.
|
||||
func (p *Parser) parseToken(expected Token) (string, error) {
|
||||
var allowedRuneFunc func(rune) bool
|
||||
switch expected {
|
||||
case FUNCTIONID, PARAMNAME:
|
||||
allowedRuneFunc = isLowercaseLetterOrDigitOrMinus
|
||||
case PARAMVALUE:
|
||||
allowedRuneFunc = isLetterOrDigitOrSymbol
|
||||
case B64ENCODED:
|
||||
allowedRuneFunc = isB64
|
||||
default:
|
||||
allowedRuneFunc = none
|
||||
}
|
||||
|
||||
token, literal := p.scan(allowedRuneFunc)
|
||||
if token&expected == 0 {
|
||||
return "", fmt.Errorf("found %q, expected '$'", literal)
|
||||
}
|
||||
|
||||
return literal, nil
|
||||
}
|
||||
|
||||
// parseFunctionId parses a function ID
|
||||
func (p *Parser) parseFunctionId() (string, error) {
|
||||
literal, err := p.parseToken(DOLLARSIGN)
|
||||
if err != nil {
|
||||
return literal, fmt.Errorf("found %q, expected '$'", literal)
|
||||
}
|
||||
|
||||
literal, err = p.parseToken(FUNCTIONID)
|
||||
if err != nil {
|
||||
return literal, fmt.Errorf("found %q, expected a function identifier", literal)
|
||||
}
|
||||
return literal, nil
|
||||
}
|
||||
|
||||
// parseHash parses a base64-encoded hash
|
||||
func (p *Parser) parseHash() (string, error) {
|
||||
// We parse the hash
|
||||
hash, err := p.parseToken(B64ENCODED)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("found %q, expected the hash", hash)
|
||||
}
|
||||
|
||||
// and make sure that the string finishes right after it
|
||||
literal, err := p.parseToken(EOF)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("found %q, expected EOF", literal)
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// parseParamsRHS parses an equal sign followed by a parameter value, returning
|
||||
// only the parameter value.
|
||||
func (p *Parser) parseParamRHS() (string, error) {
|
||||
if literal, err := p.parseToken(EQUALSIGN); err != nil {
|
||||
return literal, err
|
||||
}
|
||||
|
||||
return p.parseToken(PARAMVALUE)
|
||||
}
|
||||
|
||||
// Parse parses the [Parser]'s reader into a [PHC].
|
||||
//
|
||||
// This function will return an error along with an empty [PHC] when the provided
|
||||
// input is not PHC-compliant.
|
||||
func (p *Parser) Parse() (PHC, error) {
|
||||
// Initialize the returned PHC and its inner parameters map
|
||||
out := PHC{}
|
||||
out.Params = make(map[string]string)
|
||||
|
||||
// Start parsing: first, we expect '$functionId'
|
||||
id, err := p.parseFunctionId()
|
||||
if err != nil {
|
||||
return PHC{}, fmt.Errorf("failed to parse function ID: %w", err)
|
||||
}
|
||||
out.Id = id
|
||||
|
||||
// Now we expect either EOF, or to continue parsing with a '$'
|
||||
switch token, literal := p.scanSeparator(); token {
|
||||
case EOF:
|
||||
// Just a function identifier is valid, according to the spec
|
||||
return out, nil
|
||||
case DOLLARSIGN:
|
||||
// We continue parsing
|
||||
break
|
||||
default:
|
||||
return PHC{}, fmt.Errorf("found %q, expected '$' or EOF", literal)
|
||||
}
|
||||
|
||||
// There was a '$', so we expect now another identifier, which can either be:
|
||||
// - The version key, "v",
|
||||
// - A parameter name
|
||||
// - The salt
|
||||
// B64ENCODED is a superset of PARAMNAME (which is also a superset of "v"),
|
||||
// sso we allow the former because we don't know yet what we're parsing.
|
||||
versionKeyOrParamNameOrSalt, err := p.parseToken(B64ENCODED)
|
||||
if err != nil {
|
||||
return PHC{}, fmt.Errorf("found %q, expected the version key, 'v', a parameter name or the salt: %w", versionKeyOrParamNameOrSalt, err)
|
||||
}
|
||||
|
||||
// If it's the version key, then we know now that we are parsing
|
||||
// '$v=versionStr', and we expect now '=versionStr'
|
||||
if versionKeyOrParamNameOrSalt == "v" {
|
||||
versionStr, err := p.parseParamRHS()
|
||||
if err != nil {
|
||||
return PHC{}, fmt.Errorf("failed parsing version string: %w", err)
|
||||
}
|
||||
out.Version = versionStr
|
||||
|
||||
// Now we expect either EOF, or to continue parsing with a '$'
|
||||
switch token, literal := p.scanSeparator(); token {
|
||||
case EOF:
|
||||
// Just a function identifier + version is valid, according to the spec
|
||||
return out, nil
|
||||
case DOLLARSIGN:
|
||||
// We continue parsing
|
||||
break
|
||||
default:
|
||||
return PHC{}, fmt.Errorf("found %q, expected '$' or EOF", literal)
|
||||
}
|
||||
|
||||
// Read the next ident into the variable we had before, so we can continue
|
||||
// the logic regardless of whether this block was executed or not.
|
||||
versionKeyOrParamNameOrSalt, err = p.parseToken(B64ENCODED)
|
||||
if err != nil {
|
||||
return PHC{}, fmt.Errorf("found %q, expected a parameter name or the version key, 'v'", versionKeyOrParamNameOrSalt)
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we either didn't have a version key, or we have already parsed it,
|
||||
// so we are left with either a parameter name or the salt.
|
||||
paramNameOrSalt := versionKeyOrParamNameOrSalt
|
||||
|
||||
// We know which one by scaning the next token:
|
||||
switch token, literal := p.scanSeparator(); token {
|
||||
// If the following token is '=', then it was a parameter name, and we
|
||||
// expect now '=value'
|
||||
case EQUALSIGN:
|
||||
paramName := paramNameOrSalt
|
||||
// Additional validation for the parameter name not to have the invalid
|
||||
// value "v"
|
||||
if paramName == "v" {
|
||||
return PHC{}, fmt.Errorf("found 'v' as a parameter name, which is only allowed as the version key")
|
||||
}
|
||||
// Now we parse '=value'
|
||||
paramValue, err := p.parseToken(PARAMVALUE)
|
||||
if err != nil {
|
||||
return PHC{}, fmt.Errorf("found %q, expected a value for parameter %q", paramValue, paramName)
|
||||
}
|
||||
|
||||
// And we store the parameter
|
||||
out.Params[paramName] = paramValue
|
||||
|
||||
// If the following token is '$' or EOF, then it was the salt, so we store it,
|
||||
// and optionally parse the hash
|
||||
case DOLLARSIGN, EOF:
|
||||
salt := paramNameOrSalt
|
||||
out.Salt = salt
|
||||
|
||||
// If the token was '$', then now we expect a hash
|
||||
if token == DOLLARSIGN {
|
||||
hash, err := p.parseHash()
|
||||
if err != nil {
|
||||
return PHC{}, err
|
||||
}
|
||||
out.Hash = hash
|
||||
}
|
||||
|
||||
return out, nil
|
||||
// Otherwise, we have an error
|
||||
default:
|
||||
return PHC{}, fmt.Errorf("found %q, expected either '$', or '=' or EOF", literal)
|
||||
}
|
||||
|
||||
// If we are here, it means that we just parsed a parameter value, so now we
|
||||
// have three possibilities (in a loop):
|
||||
// - If we see EOF, then we're done!
|
||||
// - If we see a comma, then we expect another name=value pair, and we
|
||||
// restart the loop
|
||||
// - If we see '$', then we need to parse 'salt[$hash]', and we finish
|
||||
for {
|
||||
switch token, literal := p.scanSeparator(); token {
|
||||
// We're done!
|
||||
case EOF:
|
||||
return out, nil
|
||||
// Parse a name=value pair, and continue the loop
|
||||
case COMMA:
|
||||
paramName, err := p.parseToken(PARAMNAME)
|
||||
if err != nil {
|
||||
return PHC{}, err
|
||||
}
|
||||
|
||||
paramValue, err := p.parseParamRHS()
|
||||
if err != nil {
|
||||
return PHC{}, fmt.Errorf("failed parsing value from parameter %q: %w", paramName, err)
|
||||
}
|
||||
out.Params[paramName] = paramValue
|
||||
// Parse a salt and an optional hash, and finish
|
||||
case DOLLARSIGN:
|
||||
salt, err := p.parseToken(B64ENCODED)
|
||||
if err != nil {
|
||||
return PHC{}, err
|
||||
}
|
||||
out.Salt = salt
|
||||
|
||||
switch token, newLiteral := p.scanSeparator(); token {
|
||||
// If what we parsed was a $, then now we expect a $hash
|
||||
case DOLLARSIGN:
|
||||
hash, err := p.parseHash()
|
||||
if err != nil {
|
||||
return PHC{}, err
|
||||
}
|
||||
out.Hash = hash
|
||||
return out, nil
|
||||
// If what we parsed was an EOF, then we return successfully
|
||||
case EOF:
|
||||
return out, nil
|
||||
// Otherwise, we have an error
|
||||
default:
|
||||
return PHC{}, fmt.Errorf("found %q, expected either '$', or EOF", newLiteral)
|
||||
}
|
||||
default:
|
||||
return PHC{}, fmt.Errorf("found %q, expected either ',', '$' or EOF", literal)
|
||||
}
|
||||
}
|
||||
}
|
||||
584
server/channels/app/password/phcparser/parser_test.go
Normal file
584
server/channels/app/password/phcparser/parser_test.go
Normal file
@@ -0,0 +1,584 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package phcparser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
output PHC
|
||||
expectErr bool
|
||||
}{
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// Valid strings
|
||||
{
|
||||
"$argon2i$m=120,t=4294967295,p=2",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "4294967295",
|
||||
"p": "2",
|
||||
},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=2040,t=5000,p=255",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "2040",
|
||||
"t": "5000",
|
||||
"p": "255",
|
||||
},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0ZQ",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0ZQ",
|
||||
},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0ZQA",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0ZQA",
|
||||
},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,data=sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2$4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2$BwUgJHHQaynE+a4nZrYRzOllGSjjxuxNXxyNRUtI6Dlw/zlbt6PzOL8Onfqs6TcG",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
},
|
||||
Salt: "BwUgJHHQaynE+a4nZrYRzOllGSjjxuxNXxyNRUtI6Dlw/zlbt6PzOL8Onfqs6TcG",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0$4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$iHSDPHzUhPzK7rCcJgOFfg$EkCWX6pSTqWruiR0",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "iHSDPHzUhPzK7rCcJgOFfg",
|
||||
Hash: "EkCWX6pSTqWruiR0",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Params: map[string]string{},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$v=120",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "120",
|
||||
Params: map[string]string{},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$v=5$m=120,t=5000,p=2",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "5",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$/LtFjH5rVL8",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Params: map[string]string{},
|
||||
Salt: "/LtFjH5rVL8",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$/LtFjH5rVL8$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Params: map[string]string{},
|
||||
Salt: "/LtFjH5rVL8",
|
||||
Hash: "iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$v=v2$/LtFjH5rVL8",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "v2",
|
||||
Params: map[string]string{},
|
||||
Salt: "/LtFjH5rVL8",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$v=v2$/LtFjH5rVL8$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "v2",
|
||||
Params: map[string]string{},
|
||||
Salt: "/LtFjH5rVL8",
|
||||
Hash: "iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2$/LtFjH5rVL8",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
},
|
||||
Salt: "/LtFjH5rVL8",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
},
|
||||
Salt: "4fXXG0spB92WPB1NitT8/OH0VKI",
|
||||
Hash: "iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$iHSDPHzUhPzK7rCcJgOFfg$J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g",
|
||||
PHC{
|
||||
Id: "argon2i",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"m": "120",
|
||||
"t": "5000",
|
||||
"p": "2",
|
||||
"keyid": "Hj5+dsK0",
|
||||
"data": "sRlHhRmKUGzdOmXn01XmXygd5Kc",
|
||||
},
|
||||
Salt: "iHSDPHzUhPzK7rCcJgOFfg",
|
||||
Hash: "J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$pbkdf2",
|
||||
PHC{
|
||||
Id: "pbkdf2",
|
||||
Version: "",
|
||||
Params: map[string]string{},
|
||||
Salt: "",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ",
|
||||
PHC{
|
||||
Id: "pbkdf2",
|
||||
Version: "",
|
||||
Params: map[string]string{},
|
||||
Salt: "cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ",
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ$EFpj2Mnn+EbXTxZD5kv5t5Y69wzPJnDEZI3BtqlRCH0",
|
||||
PHC{
|
||||
Id: "pbkdf2",
|
||||
Version: "",
|
||||
Params: map[string]string{},
|
||||
Salt: "cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ",
|
||||
Hash: "EFpj2Mnn+EbXTxZD5kv5t5Y69wzPJnDEZI3BtqlRCH0",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$f=SHA256,w=600000,l=32$cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ$EFpj2Mnn+EbXTxZD5kv5t5Y69wzPJnDEZI3BtqlRCH0",
|
||||
PHC{
|
||||
Id: "pbkdf2",
|
||||
Version: "",
|
||||
Params: map[string]string{
|
||||
"w": "600000",
|
||||
"f": "SHA256",
|
||||
"l": "32",
|
||||
},
|
||||
Salt: "cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ",
|
||||
Hash: "EFpj2Mnn+EbXTxZD5kv5t5Y69wzPJnDEZI3BtqlRCH0",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$v=5$w=600000,f=SHA256,l=32$iHSDPHzUhPzK7rCcJgOFfg$J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g",
|
||||
PHC{
|
||||
Id: "pbkdf2",
|
||||
Version: "5",
|
||||
Params: map[string]string{
|
||||
"w": "600000",
|
||||
"f": "SHA256",
|
||||
"l": "32",
|
||||
},
|
||||
Salt: "iHSDPHzUhPzK7rCcJgOFfg",
|
||||
Hash: "J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g",
|
||||
},
|
||||
false,
|
||||
},
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// Invalid strings
|
||||
{
|
||||
"",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"$",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$m=",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$m=120,",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$m=120,t=",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$$$$",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"$pbkdf2$v=5$v=600000,f=SHA256,l=32$iHSDPHzUhPzK7rCcJgOFfg$J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g",
|
||||
PHC{},
|
||||
true,
|
||||
},
|
||||
// We limit the input to MaxRunes, so any runes after the limit are ignored
|
||||
{
|
||||
"$pbkdf2$cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ" + strings.Repeat("a", MaxRunes),
|
||||
PHC{
|
||||
Id: "pbkdf2",
|
||||
Version: "",
|
||||
Params: map[string]string{},
|
||||
Salt: "cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ" + strings.Repeat("a", MaxRunes-len("$pbkdf2$cGFsZXN0aW5lIHdpbGwgYmUgZnJlZQ")),
|
||||
Hash: "",
|
||||
},
|
||||
false,
|
||||
},
|
||||
// Edge case
|
||||
{
|
||||
// This is a bcrypt hashed password: it looks like PHC, but it is not:
|
||||
// The format is $xy$n$salthash, where:
|
||||
// - $ is the literal '$' (1 byte)
|
||||
// - x is the major version (1 byte)
|
||||
// - y is the minor version (0 or 1 byte)
|
||||
// - $ is the literal '$' (1 byte)
|
||||
// - n is the cost (2 bytes)
|
||||
// - $ is the literal '$' (1 byte)
|
||||
// - salt is the encoded salt (22 bytes)
|
||||
// - hash is the encoded hash (31 bytes)
|
||||
// In total, 60 bytes (59 if there is no minor version)
|
||||
//
|
||||
// But this is not PHC-compliant: xy is not the function id, n is not
|
||||
// a parameter name=value, nor the version, and there is no '$'
|
||||
// separating the salt and the hash.
|
||||
//
|
||||
// However, it is *technically* PHC-compliant:
|
||||
// - xy is parsed as a function ID
|
||||
// - n is parsed as the salt
|
||||
// - salthash is parsed as the hash
|
||||
//
|
||||
// This is somewhat of an edge-case, and should probably be rejected
|
||||
// as non-parseable because "2a" is *not* a known function ID, but
|
||||
// we don't check for specific strings as of today
|
||||
"$2a$10$z0OlN1MpiLVlLTyE1xtEjOJ6/xV95RAwwIUaYKQBAqoeyvPgLEnUa",
|
||||
PHC{
|
||||
Id: "2a",
|
||||
Version: "",
|
||||
Params: map[string]string{},
|
||||
Salt: "10",
|
||||
Hash: "z0OlN1MpiLVlLTyE1xtEjOJ6/xV95RAwwIUaYKQBAqoeyvPgLEnUa",
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
p := New(strings.NewReader(tc.input))
|
||||
phc, err := p.Parse()
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, phc, tc.output)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user