|
| 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