Skip to content

Commit d257d4c

Browse files
committed
feat: add phc api
1 parent 8cdc056 commit d257d4c

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

lib/phc.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// NOTE: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
2+
3+
const NAME_RE = /^[\da-z-]{1,32}$/,
4+
VALUE_RE = /^[\d+./A-Za-z-]+$/,
5+
PHC_RE = new RegExp( String.raw`^` +
6+
String.raw`\$(?<id>[\da-z-]{1,32})` + // id
7+
String.raw`(?:\$v=(?<version>\d+))?` + // version
8+
String.raw`(?:\$(?<params>[\da-z-]{1,32}=[\d+./A-Za-z-]+(?:,[\da-z-]{1,32}=[\d+./A-Za-z-]+)*))?` + // params
9+
String.raw`(?:\$(?<salt>[\d+/A-Za-z]+))?` + // salt
10+
String.raw`(?:\$(?<hash>[\d+/A-Za-z]+))?` + // hash
11+
String.raw`$` );
12+
13+
// public
14+
export function encode ( { id, version, params, salt, hash, defaultParams, sortParams = true } = {} ) {
15+
if ( !NAME_RE.test( id ) ) throw "ID must be in the kebab-case";
16+
17+
var string = "$" + id;
18+
19+
// version
20+
if ( version != null ) {
21+
if ( typeof version !== "number" ) throw "Version must be a number";
22+
23+
if ( defaultParams?.v == null || Number( defaultParams.v ) !== version ) {
24+
string += "$v=" + version;
25+
}
26+
}
27+
28+
// params
29+
if ( params ) {
30+
const values = [];
31+
32+
params = Object.entries( params );
33+
34+
if ( sortParams ) {
35+
params = params.sort( ( a, b ) => a[ 0 ].localeCompare( b[ 0 ] ) );
36+
}
37+
38+
for ( const [ name, value ] of params ) {
39+
40+
// name
41+
if ( !NAME_RE.test( name ) ) throw "Name must be in the kebab-case";
42+
if ( name === "v" ) throw 'Parameter name should not be a "v"';
43+
44+
// value
45+
if ( !VALUE_RE.test( value ) ) throw "Parameter value is not valid";
46+
47+
if ( defaultParams?.[ name ] == null || String( defaultParams[ name ] ) !== String( value ) ) {
48+
values.push( name + "=" + value );
49+
}
50+
}
51+
52+
if ( values.length ) {
53+
string += "$" + values.join( "," );
54+
}
55+
}
56+
57+
// salt
58+
if ( salt != null ) {
59+
if ( typeof salt === "string" ) {
60+
if ( !VALUE_RE.test( salt ) ) throw "Salt is not valid";
61+
}
62+
else if ( salt instanceof Buffer ) {
63+
salt = salt.toString( "base64" ).replace( /=+$/, "" );
64+
}
65+
else {
66+
throw "Salt is not valid";
67+
}
68+
69+
if ( salt.length ) {
70+
string += "$" + salt;
71+
72+
// hash
73+
if ( hash != null ) {
74+
if ( hash instanceof Buffer ) {
75+
hash = hash.toString( "base64" ).replace( /=+$/, "" );
76+
}
77+
else {
78+
throw "Hash is not valid";
79+
}
80+
81+
if ( hash.length ) {
82+
string += "$" + hash;
83+
}
84+
}
85+
}
86+
}
87+
88+
return string;
89+
}
90+
91+
export function decode ( string, { defaultParams, decodeSalt = true, decodeHash = true } = {} ) {
92+
const match = string.match( PHC_RE );
93+
if ( !match ) throw "PHC string is not valid";
94+
95+
const data = {
96+
"id": match.groups.id,
97+
"version": undefined,
98+
"params": undefined,
99+
"salt": match.groups.salt
100+
? ( decodeSalt
101+
? Buffer.from( match.groups.salt, "base64" )
102+
: match.groups.salt )
103+
: undefined,
104+
"hash": match.groups.hash
105+
? ( decodeHash
106+
? Buffer.from( match.groups.hash, "base64" )
107+
: match.groups.hash )
108+
: undefined,
109+
};
110+
111+
// apply default values
112+
if ( defaultParams ) {
113+
for ( const [ name, value ] of Object.entries( defaultParams ) ) {
114+
if ( name === "v" ) {
115+
data.version = value == null
116+
? value
117+
: Number( value );
118+
}
119+
else {
120+
data.params ??= {};
121+
data.params[ name ] = value;
122+
}
123+
}
124+
}
125+
126+
// version
127+
if ( match.groups.version ) {
128+
data.version = Number( match.groups.version );
129+
}
130+
131+
// params
132+
if ( match.groups.params ) {
133+
data.params ??= {};
134+
135+
for ( const param of match.groups.params.split( "," ) ) {
136+
const [ name, value ] = param.split( "=" );
137+
138+
if ( name === "v" ) throw "PHC params are not valid";
139+
140+
data.params[ name ] = value;
141+
}
142+
}
143+
144+
return data;
145+
}

0 commit comments

Comments
 (0)