UNPKG

@softvisio/utils

Version:
174 lines (141 loc) 5.2 kB
// NOTE: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md const NAME_RE = /^[\da-z-]{1,32}$/, VALUE_RE = /^[\d+./A-Za-z-]+$/, INTEGER_RE = /^-?(?:0|[1-9]\d{0,15})$/, PHC_RE = new RegExp( String.raw`^` + String.raw`\$(?<id>[\da-z-]{1,32})` + // id String.raw`(?:\$v=(?<version>\d+))?` + // version String.raw`(?:\$(?<params>[\da-z-]{1,32}=[\d+./A-Za-z-]+(?:,[\da-z-]{1,32}=[\d+./A-Za-z-]+)*))?` + // params String.raw`(?:\$(?<salt>[\d+./A-Za-z-]+))?` + // salt String.raw`(?:\$(?<hash>[\d+/A-Za-z]+))?` + // hash String.raw`$` ); // public export function toPhc ( { id, version, params, salt, hash, defaultParams, sortParams } = {} ) { if ( !NAME_RE.test( id ) ) throw "ID must be in the kebab-case"; var string = "$" + id; // version if ( version != null ) { if ( typeof version !== "number" ) throw "Version must be a number"; if ( defaultParams?.v == null || Number( defaultParams.v ) !== version ) { string += "$v=" + version; } } // params if ( params ) { const values = []; params = Object.entries( params ); if ( sortParams ) { params = params.sort( ( a, b ) => a[ 0 ].localeCompare( b[ 0 ] ) ); } let name, value; for ( [ name, value ] of params ) { // name if ( !NAME_RE.test( name ) ) throw "Name must be in the kebab-case"; if ( name === "v" ) throw 'Parameter name should not be a "v"'; // value value ??= defaultParams?.[ name ]; if ( value == null ) { continue; } else if ( value instanceof Buffer ) { value = value.toString( "base64" ).replace( /=+$/, "" ); } else if ( !VALUE_RE.test( value ) ) { throw "Parameter value is not valid"; } if ( defaultParams?.[ name ] == null || String( defaultParams[ name ] ) !== String( value ) ) { values.push( name + "=" + value ); } } if ( values.length ) { string += "$" + values.join( "," ); } } // salt if ( salt != null ) { if ( typeof salt === "string" ) { if ( !VALUE_RE.test( salt ) ) throw "Salt is not valid"; } else if ( salt instanceof Buffer ) { salt = salt.toString( "base64" ).replace( /=+$/, "" ); } else { throw "Salt is not valid"; } if ( salt.length ) { string += "$" + salt; // hash if ( hash != null ) { if ( hash instanceof Buffer ) { hash = hash.toString( "base64" ).replace( /=+$/, "" ); } else { throw "Hash is not valid"; } if ( hash.length ) { string += "$" + hash; } } } } return string; } export function fromPhc ( string, { defaultParams, decodeNumbers = true, saltEncoding = "base64" } = {} ) { const match = string.match( PHC_RE ); if ( !match ) throw "PHC string is not valid"; const data = { "id": match.groups.id, "version": undefined, "params": undefined, "salt": match.groups.salt ? Buffer.from( match.groups.salt, saltEncoding ) : undefined, "hash": match.groups.hash ? Buffer.from( match.groups.hash, "base64" ) : undefined, }; // apply default values if ( defaultParams ) { for ( const [ name, value ] of Object.entries( defaultParams ) ) { if ( name === "v" ) { data.version = value == null ? value : Number( value ); } else { data.params ??= {}; if ( decodeNumbers && typeof value === "string" && INTEGER_RE.test( value ) ) { data.params[ name ] = Number.parseInt( value ); if ( !Number.isSafeInteger( data.params[ name ] ) ) { data.params[ name ] = value; } } else { data.params[ name ] = value; } } } } // version if ( match.groups.version ) { data.version = Number( match.groups.version ); } // params if ( match.groups.params ) { data.params ??= {}; for ( const param of match.groups.params.split( "," ) ) { const [ name, value ] = param.split( "=" ); if ( name === "v" ) continue; if ( decodeNumbers && INTEGER_RE.test( value ) ) { data.params[ name ] = Number.parseInt( value ); if ( !Number.isSafeInteger( data.params[ name ] ) ) { data.params[ name ] = value; } } else { data.params[ name ] = value; } } } return data; }