UNPKG

@iden3/js-iden3-core

Version:

Low level API to create and manipulate iden3 Claims.

432 lines (352 loc) 13.1 kB
import { IDID, Param, initDIDParams } from './types'; import { StringUtils } from '../utils'; // a step in the parser state machine that returns the next step type ParserStep = () => ParserStep | null; export class Parser { currentIndex = 0; // index in the input which the parser is currently processing: out: IDID = { ...initDIDParams }; // the output DID that the parser will assemble as it steps through its state machine // an error in the parser state machine constructor(private readonly input: string) {} checkLength(): ParserStep | null { const inputLength = this.input.length; if (inputLength < 7) { throw new Error('input length is less than 7'); } return this.parseScheme.bind(this); } // parseScheme is a parserStep that validates that the input begins with 'did:' parseScheme(): ParserStep | null { const currentIndex = 3; // 4 bytes in 'did:', i.e index 3 // the grammar requires `did:` prefix if (this.input.slice(0, currentIndex + 1) !== 'did:') { throw new Error("input does not begin with 'did:' prefix"); } this.currentIndex = currentIndex; return this.parseMethod.bind(this); } parseMethod(): ParserStep | null { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; const startIndex = currentIndex; for (;;) { if (currentIndex === inputLength) { // we got to the end of the input and didn't find a second ':' throw new Error('input does not have a second `:` marking end of method name'); } // read the input character at currentIndex const char = input[currentIndex]; if (char === ':') { // we've found the second : in the input that marks the end of the method if (currentIndex === startIndex) { // return error is method is empty, ex- did::1234 throw new Error(`method is empty, ${currentIndex}`); } break; } // as per the grammar method can only be made of digits 0-9 or small letters a-z if (StringUtils.isNotDigit(char) && StringUtils.isNotSmallLetter(char)) { throw new Error(`"character is not a-z OR 0-9, ${currentIndex}`); } // move to the next char currentIndex = currentIndex + 1; } // set parser state this.currentIndex = currentIndex; this.out.method = input.slice(startIndex, currentIndex); // method is followed by specific-idstring, parse that next return this.parseId.bind(this); } parseId(): ParserStep | null { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; const startIndex = currentIndex; let next: ParserStep | null = null; for (;;) { if (currentIndex === inputLength) { // we've reached end of input, no next state next = null; break; } const char = input[currentIndex]; if (char === ':') { // encountered : input may have another idstring, parse ID again next = this.parseId; break; } if (char === ';') { // encountered ; input may have a parameter, parse that next next = this.parseParamName; break; } if (char === '/') { // encountered / input may have a path following specific-idstring, parse that next next = this.parsePath; break; } if (char === '?') { // encountered ? input may have a query following specific-idstring, parse that next next = this.parseQuery; break; } if (char === '#') { // encountered # input may have a fragment following specific-idstring, parse that next next = this.parseFragment; break; } // make sure current char is a valid idchar // idchar = ALPHA / DIGIT / "." / "-" if (StringUtils.isNotValidIDChar(char)) { throw new Error(`byte is not ALPHA OR DIGIT OR '.' OR '-', ${currentIndex}`); } // move to the next char currentIndex = currentIndex + 1; } if (currentIndex === startIndex) { // idstring length is zero // from the grammar: // idstring = 1*idchar // return error because idstring is empty, ex- did:a::123:456 throw new Error(`idstring must be at least one char long, ${currentIndex}`); } // set parser state this.currentIndex = currentIndex; this.out.idStrings = [...this.out.idStrings, input.slice(startIndex, currentIndex)]; // return the next parser step return next ? next.bind(this) : null; } parseParamName(): ParserStep | null { const input = this.input; const startIndex = this.currentIndex + 1; const next = this.paramTransition(); const currentIndex = this.currentIndex; if (currentIndex === startIndex) { throw new Error(`Param name must be at least one char long, ${currentIndex}`); } // Create a new param with the name this.out.params = [...this.out.params, new Param(input.slice(startIndex, currentIndex), '')]; // return the next parser step return next ? next.bind(this) : null; } parseParamValue(): ParserStep | null { const input = this.input; const startIndex = this.currentIndex + 1; const next = this.paramTransition(); const currentIndex = this.currentIndex; this.out.params[this.out.params.length - 1].value = input.slice(startIndex, currentIndex); return next ? next.bind(this) : null; } paramTransition(): ParserStep | null { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; let indexIncrement: number; let next: ParserStep | null; let percentEncoded: boolean; for (;;) { if (currentIndex === inputLength) { // we've reached end of input, no next state next = null; break; } const char = input[currentIndex]; if (char === ';') { // encountered : input may have another param, parse paramName again next = this.parseParamName; break; } // Separate steps for name and value? if (char === '=') { // parse param value next = this.parseParamValue; break; } if (char === '/') { // encountered / input may have a path following current param, parse that next next = this.parsePath; break; } if (char === '?') { // encountered ? input may have a query following current param, parse that next next = this.parseQuery; break; } if (char == '#') { // encountered # input may have a fragment following current param, parse that next next = this.parseFragment; break; } if (char == '%') { // a % must be followed by 2 hex digits if ( currentIndex + 2 >= inputLength || StringUtils.isNotHexDigit(input[currentIndex + 1]) || StringUtils.isNotHexDigit(input[currentIndex + 2]) ) { throw new Error(`% is not followed by 2 hex digits', ${currentIndex}`); } // if we got here, we're dealing with percent encoded char, jump three chars percentEncoded = true; indexIncrement = 3; } else { // not percent encoded percentEncoded = false; indexIncrement = 1; } // make sure current char is a valid param-char // idchar = ALPHA / DIGIT / "." / "-" if (!percentEncoded && StringUtils.isNotValidParamChar(char)) { throw new Error(`character is not allowed in param - ${char}', ${currentIndex}`); } // move to the next char currentIndex = currentIndex + indexIncrement; } // set parser state this.currentIndex = currentIndex; return next ? next.bind(this) : null; } parsePath(): ParserStep | null { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; const startIndex = currentIndex; let indexIncrement: number; let next: ParserStep | null; let percentEncoded: boolean; for (;;) { if (currentIndex === inputLength) { next = null; break; } const char = input[currentIndex]; if (char === '/') { // encountered / input may have another path segment, try to parse that next next = this.parsePath; break; } if (char === '?') { // encountered ? input may have a query following path, parse that next next = this.parseQuery; break; } if (char === '%') { // a % must be followed by 2 hex digits if ( currentIndex + 2 >= inputLength || StringUtils.isNotHexDigit(input[currentIndex + 1]) || StringUtils.isNotHexDigit(input[currentIndex + 2]) ) { throw new Error(`% is not followed by 2 hex digits, ${currentIndex}`); } // if we got here, we're dealing with percent encoded char, jump three chars percentEncoded = true; indexIncrement = 3; } else { // not percent encoded percentEncoded = false; indexIncrement = 1; } // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" if (!percentEncoded && StringUtils.isNotValidPathChar(char)) { throw new Error(`character is not allowed in path, ${currentIndex}`); } // move to the next char currentIndex = currentIndex + indexIncrement; } if (currentIndex == startIndex && this.out.pathSegments.length === 0) { throw new Error(`first path segment must have at least one character, ${currentIndex}`); } // update parser state this.currentIndex = currentIndex; this.out.pathSegments = [...this.out.pathSegments, input.slice(startIndex, currentIndex)]; return next ? next.bind(this) : null; } parseQuery(): ParserStep | null { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; const startIndex = currentIndex; let indexIncrement: number; let next: ParserStep | null = null; let percentEncoded: boolean; for (;;) { if (currentIndex === inputLength) { break; } const char = input[currentIndex]; if (char === '#') { // encountered # input may have a fragment following the query, parse that next next = this.parseFragment; break; } if (char === '%') { // a % must be followed by 2 hex digits if ( currentIndex + 2 >= inputLength || StringUtils.isNotHexDigit(input[currentIndex + 1]) || StringUtils.isNotHexDigit(input[currentIndex + 2]) ) { throw new Error(`% is not followed by 2 hex digits, ${currentIndex}`); } // if we got here, we're dealing with percent encoded char, jump three chars percentEncoded = true; indexIncrement = 3; } else { // not percent encoded percentEncoded = false; indexIncrement = 1; } if (!percentEncoded && StringUtils.isNotValidQueryOrFragmentChar(char)) { throw new Error(`character is not allowed in query - ${char}`); } // move to the next char currentIndex = currentIndex + indexIncrement; } // update parser state this.currentIndex = currentIndex; this.out.query = input.slice(startIndex, currentIndex); return next ? next.bind(this) : null; } parseFragment(): ParserStep | null { const input = this.input; const inputLength = this.input.length; let currentIndex = this.currentIndex + 1; const startIndex = currentIndex; let indexIncrement: number; let percentEncoded: boolean; for (;;) { if (currentIndex === inputLength) { break; } const char = input[currentIndex]; if (char === '%') { // a % must be followed by 2 hex digits if ( currentIndex + 2 >= inputLength || StringUtils.isNotHexDigit(input[currentIndex + 1]) || StringUtils.isNotHexDigit(input[currentIndex + 2]) ) { throw new Error(`% is not followed by 2 hex digits, ${currentIndex}`); } // if we got here, we're dealing with percent encoded char, jump three chars percentEncoded = true; indexIncrement = 3; } else { // not percent encoded percentEncoded = false; indexIncrement = 1; } if (!percentEncoded && StringUtils.isNotValidQueryOrFragmentChar(char)) { throw new Error(`character is not allowed in fragment - ${char}`); } // move to the next char currentIndex = currentIndex + indexIncrement; } // update parser state this.currentIndex = currentIndex; this.out.fragment = input.slice(startIndex, currentIndex); // no more parsing needed after a fragment, // cause the state machine to exit by returning nil return null; } }