@openzeppelin/contracts-ui-builder-adapter-stellar
Version:
Stellar Adapter for Contracts UI Builder
249 lines (217 loc) • 7.05 kB
text/typescript
/**
* Safe type parsing utilities for Stellar generic types.
*
* This module provides secure, performant alternatives to regex-based parsing
* to prevent ReDoS (Regular Expression Denial of Service) attacks when processing
* Stellar contract parameter types like Vec<T>, Map<K,V>, and Option<T>.
*
* Uses iterative parsing instead of vulnerable regex patterns with greedy quantifiers.
*/
/**
* Configuration constants for safe parsing
*/
const PARSING_LIMITS = {
/** Maximum depth for nested generic types to prevent stack overflow */
MAX_NESTING_DEPTH: 10,
/** Maximum string length for type parsing to prevent DoS */
MAX_TYPE_STRING_LENGTH: 1000,
} as const;
/**
* Result type for type extraction operations
*/
type ExtractionResult<T> = T | null;
/**
* Safely extracts the inner type from a Stellar Vec type.
*
* @param parameterType - The parameter type (e.g., 'Vec<U32>', 'Vec<Vec<Address>>')
* @returns The inner type (e.g., 'U32', 'Vec<Address>') or null if not a Vec type
*
* @example
* ```typescript
* extractVecElementType('Vec<U32>') // → 'U32'
* extractVecElementType('Vec<Vec<Address>>') // → 'Vec<Address>'
* extractVecElementType('U32') // → null
* ```
*/
export function extractVecElementType(parameterType: string): ExtractionResult<string> {
if (!isValidTypeString(parameterType) || !parameterType.startsWith('Vec<')) {
return null;
}
return extractGenericInnerType(parameterType, 'Vec');
}
/**
* Safely extracts the key and value types from a Stellar Map type.
*
* @param parameterType - The parameter type (e.g., 'Map<Symbol, Bytes>', 'Map<U32, Vec<Address>>')
* @returns An object with keyType and valueType, or null if not a Map type
*
* @example
* ```typescript
* extractMapTypes('Map<U32, Address>') // → { keyType: 'U32', valueType: 'Address' }
* extractMapTypes('Map<Symbol, Vec<U32>>') // → { keyType: 'Symbol', valueType: 'Vec<U32>' }
* extractMapTypes('U32') // → null
* ```
*/
export function extractMapTypes(
parameterType: string
): ExtractionResult<{ keyType: string; valueType: string }> {
if (!isValidTypeString(parameterType) || !parameterType.startsWith('Map<')) {
return null;
}
const innerContent = extractGenericInnerType(parameterType, 'Map');
if (!innerContent) {
return null;
}
// Find the top-level comma that separates key and value types
const commaIndex = findTopLevelComma(innerContent);
if (commaIndex === -1) {
return null;
}
const keyType = innerContent.slice(0, commaIndex).trim();
const valueType = innerContent.slice(commaIndex + 1).trim();
// Validate both types are non-empty and don't contain invalid characters
if (!keyType || !valueType || hasInvalidCharacters(keyType) || hasInvalidCharacters(valueType)) {
return null;
}
return { keyType, valueType };
}
/**
* Safely extracts the inner type from a Stellar Option type.
*
* @param parameterType - The parameter type (e.g., 'Option<U32>', 'Option<Vec<Address>>')
* @returns The inner type or null if not an Option type
*
* @example
* ```typescript
* extractOptionElementType('Option<U32>') // → 'U32'
* extractOptionElementType('Option<Vec<Address>>') // → 'Vec<Address>'
* extractOptionElementType('U32') // → null
* ```
*/
export function extractOptionElementType(parameterType: string): ExtractionResult<string> {
if (!isValidTypeString(parameterType) || !parameterType.startsWith('Option<')) {
return null;
}
return extractGenericInnerType(parameterType, 'Option');
}
/**
* Generic function to extract inner content from generic types.
*
* @param parameterType - The full parameter type
* @param genericName - The generic type name (e.g., 'Vec', 'Map', 'Option')
* @returns The inner content or null if extraction fails
*/
function extractGenericInnerType(
parameterType: string,
genericName: string
): ExtractionResult<string> {
const prefix = `${genericName}<`;
if (!parameterType.startsWith(prefix) || !parameterType.endsWith('>')) {
return null;
}
const innerContent = parameterType.slice(prefix.length, -1);
if (!innerContent || hasInvalidCharacters(innerContent)) {
return null;
}
// Validate that brackets are properly balanced
if (!isBalancedBrackets(innerContent)) {
return null;
}
return innerContent.trim();
}
/**
* Finds the first top-level comma in a type string, ignoring commas inside nested brackets.
*
* @param content - The content to search in
* @returns The index of the top-level comma, or -1 if not found
*
* @example
* ```typescript
* findTopLevelComma('U32, Address') // → 3
* findTopLevelComma('Vec<U32, U64>, Address') // → 13 (after the first >)
* ```
*/
function findTopLevelComma(content: string): number {
let angleLevel = 0;
for (let i = 0; i < content.length; i++) {
const char = content[i];
switch (char) {
case '<':
angleLevel++;
break;
case '>':
angleLevel--;
if (angleLevel < 0) return -1; // Malformed input
break;
case ',':
if (angleLevel === 0) {
return i;
}
break;
}
}
return -1;
}
/**
* Checks if brackets are properly balanced in a string and enforces nesting depth limits.
*
* @param content - The content to validate
* @returns True if brackets are balanced and within depth limits, false otherwise
*/
function isBalancedBrackets(content: string): boolean {
let angleLevel = 0;
let maxNesting = 0;
for (const char of content) {
switch (char) {
case '<':
angleLevel++;
maxNesting = Math.max(maxNesting, angleLevel);
// Enforce nesting depth limit
if (maxNesting > PARSING_LIMITS.MAX_NESTING_DEPTH) {
return false;
}
break;
case '>':
angleLevel--;
if (angleLevel < 0) return false;
break;
}
}
return angleLevel === 0;
}
/**
* Validates that a type string is safe to process.
*
* @param typeString - The type string to validate
* @returns True if the type string is safe to process
*/
export function isValidTypeString(typeString: string): boolean {
if (!typeString || typeof typeString !== 'string') {
return false;
}
// Length check to prevent DoS
if (typeString.length > PARSING_LIMITS.MAX_TYPE_STRING_LENGTH) {
return false;
}
// Check for invalid characters
if (hasInvalidCharacters(typeString)) {
return false;
}
// Validate bracket balance
return isBalancedBrackets(typeString);
}
/**
* Checks if a string contains invalid characters for type names.
*
* @param str - The string to check
* @returns True if the string contains invalid characters
*/
function hasInvalidCharacters(str: string): boolean {
// No control characters or line breaks
if (/[\x00-\x1F\x7F\r\n]/.test(str)) {
return true;
}
// Only allow alphanumeric, angle brackets, commas, underscores, and spaces
// Note: Parentheses are NOT allowed in Stellar type strings
return !/^[A-Za-z0-9<>,\s_]+$/.test(str);
}