1 // 2 // Copyright (c) 2010-2024 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 using System; 8 using System.Collections.Generic; 9 using System.Globalization; 10 using System.Linq; 11 using System.Reflection; 12 using Antmicro.Renode.Utilities; 13 14 namespace Antmicro.Renode.Peripherals.Network 15 { 16 public abstract partial class AtCommandModem 17 { 18 [ArgumentParser] ParseString(string str)19 protected string ParseString(string str) 20 { 21 if(str.Length < 2 || str.First() != '"' || str.Last() != '"') 22 { 23 return str; // unquoted string is allowed 24 } 25 else 26 { 27 return str.Substring(1, str.Length - 2); 28 } 29 } 30 31 // This enum parser is looked up as special case and its signature is incomatible 32 // with other argument parsing functions, so it has no [ArgumentParser] annotation 33 // We don't use SmartParser here because it's case sensitive and doesn't handle spaces in arguments ParseStringEnum(string str, Type enumType)34 protected object ParseStringEnum(string str, Type enumType) 35 { 36 var strValue = ParseString(str).Replace(" ", ""); 37 try 38 { 39 // We use Parse and catch the exception because TryParse is generic and 40 // so doesn't take the enum type as a normal argument. 41 return Enum.Parse(enumType, strValue, true); 42 } 43 catch(Exception e) 44 { 45 throw new ArgumentOutOfRangeException($"Enum {enumType.FullName} does not contain '{strValue}'", e); 46 } 47 } 48 ParseArgument(string argument, Type parameterType)49 private object ParseArgument(string argument, Type parameterType) 50 { 51 // If the type is nullable, get its underlying type 52 var underlyingType = Nullable.GetUnderlyingType(parameterType); 53 parameterType = underlyingType ?? parameterType; 54 55 if(argument == "") 56 { 57 return Type.Missing; 58 } 59 else if(argumentParsers.TryGetValue(parameterType, out var parser)) 60 { 61 return parser(argument); 62 } 63 else if(parameterType.IsEnum) 64 { 65 // Try parsing as a string enum, i.e. "TCP" -> Tcp 66 try 67 { 68 return ParseStringEnum(argument, parameterType); 69 } 70 catch(ArgumentException) 71 { 72 // If this fails, do nothing - number -> enum parsing, i.e. 0 -> Tcp will be attempted by SmartParser 73 } 74 } 75 76 // Use SmartParser for numeric types and number -> enum parsing 77 // Note: this is not a part of the else-if chain above because it also handles string -> enum parsing 78 if(SmartParser.Instance.TryParse(argument, parameterType, out var result)) 79 { 80 return result; 81 } 82 // If we failed to parse this enum as a string above, and SmartParser also failed to parse it 83 // as a number, this means the argument itself is invalid. 84 if(parameterType.IsEnum) 85 { 86 throw new ArgumentException($"Enum argument '{argument}' is invalid"); 87 } 88 89 // If none of the parsers covered this parameter, this is a model implementation error. 90 throw new NotSupportedException($"No argument parser found for {parameterType.FullName}"); 91 } 92 ParseArguments(string argumentsString, IEnumerable<ParameterInfo> parameters)93 private object[] ParseArguments(string argumentsString, IEnumerable<ParameterInfo> parameters) 94 { 95 var parameterTypes = parameters.Select(p => p.ParameterType); 96 Type arrayParameterType = null; 97 var lastParameter = parameters.LastOrDefault(); 98 if(lastParameter?.IsDefined(typeof(ParamArrayAttribute)) ?? false) 99 { 100 arrayParameterType = lastParameter.ParameterType.GetElementType(); 101 parameterTypes = parameterTypes.Take(parameterTypes.Count() - 1); 102 } 103 104 var arguments = argumentsString.Split(','); 105 var parsedArguments = arguments.Zip(parameterTypes, (arg, type) => ParseArgument(arg.Trim(), type)); 106 if(arrayParameterType != null) 107 { 108 var arrayElements = arguments 109 .Skip(parsedArguments.Count()) 110 .Select(arg => ParseArgument(arg.Trim(), arrayParameterType)).ToArray(); 111 112 // We make a new array and copy into it so that we get (for example) 113 // int[] or string[] as appropriate. arrayElements is always an object[]. 114 var arrayArgument = Array.CreateInstance(arrayParameterType, arrayElements.Length); 115 Array.Copy(arrayElements, arrayArgument, arrayElements.Length); 116 parsedArguments = parsedArguments.Append(new [] { arrayArgument } ); 117 } 118 return parsedArguments.ToArray(); 119 } 120 GetArgumentParsers()121 private Dictionary<Type, Func<string, object>> GetArgumentParsers() 122 { 123 // We don't inherit the [ArgumentParser] attribute in order to allow "hiding" parsers 124 // in subclasses by overriding them and not marking them with [ArgumentParser] 125 return this.GetType().GetMethodsWithAttribute<ArgumentParserAttribute>(inheritAttribute: false) 126 .Select(ma => ma.Method) 127 .ToDictionary<MethodInfo, Type, Func<string, object>>(m => m.ReturnType, m => 128 { 129 // We can't just do m.CreateDelegate(typeof(Func<string, object>)) because 130 // that wouldn't work for parse functions that return value types 131 var delType = typeof(Func<,>).MakeGenericType(typeof(string), m.ReturnType); 132 dynamic del = m.CreateDelegate(delType, this); 133 // This lambda is only to box value types 134 return s => del(s); 135 }); 136 } 137 138 [AttributeUsage(AttributeTargets.Method)] 139 protected class ArgumentParserAttribute : Attribute 140 { 141 } 142 143 private class ParsedCommand 144 { ParsedCommand(string command)145 public ParsedCommand(string command) 146 { 147 if(!command.StartsWith("AT", true, CultureInfo.InvariantCulture)) 148 { 149 throw new ArgumentException($"Command '{command}' does not start with AT"); 150 } 151 152 if(command.EndsWith("=?")) // Test command 153 { 154 Command = command.Substring(0, command.Length - 2); 155 Type = CommandType.Test; 156 } 157 else if(command.EndsWith("?")) // Read command 158 { 159 Command = command.Substring(0, command.Length - 1); 160 Type = CommandType.Read; 161 } 162 else if(command.Contains("=")) // Write command 163 { 164 var parts = command.Split(new [] { '=' }, 2); 165 Command = parts[0]; 166 Arguments = parts[1]; 167 Type = CommandType.Write; 168 } 169 else // Execution command or basic command 170 { 171 // We assume that basic commands can have at most one single-digit argument 172 // (like ATE or ATE0). Basic commands are treated as execution commands. 173 string arguments = ""; 174 if(char.IsDigit(command.Last())) 175 { 176 arguments = command.Last().ToString(); 177 command = command.Substring(0, command.Length - 1); 178 } 179 Command = command; 180 Arguments = arguments; 181 Type = CommandType.Execution; 182 } 183 } 184 TryParse(string command, out ParsedCommand parsed)185 public static bool TryParse(string command, out ParsedCommand parsed) 186 { 187 try 188 { 189 parsed = new ParsedCommand(command); 190 return true; 191 } 192 catch(Exception) 193 { 194 parsed = default(ParsedCommand); 195 return false; 196 } 197 } 198 199 public string Command 200 { 201 get => command; 202 private set => command = value.ToUpper(); 203 } 204 public CommandType Type { get; } 205 public string Arguments { get; } = ""; 206 207 private string command; 208 } 209 } 210 } 211