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