mirror of
https://github.com/JasonYANG170/logicanalyzer.git
synced 2024-11-27 05:56:30 +00:00
661 lines
25 KiB
C#
661 lines
25 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.SymbolStore;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using static SignalDescriptionLanguage.SDLParser;
|
|
|
|
namespace SignalDescriptionLanguage
|
|
{
|
|
public static class SDLParser
|
|
{
|
|
const string numExpr = "(0x)?[0-9a-fA-F]+";
|
|
const string valExpr = $"([hlHL!=]{numExpr})";
|
|
const string byteExpr = $"([bB]{numExpr})";
|
|
const string strExpr = "([sS]\"([^\"\\\\]|\\\\.)+\")";
|
|
const string nameExpr = "(\\[[0-9a-zA-Z]+\\])";
|
|
|
|
static Regex initialReg = new Regex("^\\$[01]$");
|
|
static Regex valueReg = new Regex($"^{valExpr}$");
|
|
static Regex byteReg = new Regex($"^{byteExpr}$");
|
|
static Regex strReg = new Regex($"^{strExpr}$");
|
|
static Regex nameReg = new Regex($"^{nameExpr}$");
|
|
|
|
static Regex groupReg = new($"^{{((<[0-9a-zA-Z]+>,)?((\\s*({nameExpr}|{valExpr}|{byteExpr}|{strExpr})\\s*[,}}])+))(?<=}})({numExpr})$");
|
|
|
|
static Regex commentReg = new Regex("//.*$", RegexOptions.Multiline);
|
|
static Regex commentBlockReg = new Regex("/\\*.*?\\*/");
|
|
|
|
public static IToken[] GetTokens(string Input)
|
|
{
|
|
|
|
string clean = commentReg.Replace(Input, "");
|
|
clean = clean.Replace("\r", "").Replace("\n", "");
|
|
clean = commentBlockReg.Replace(clean, "");
|
|
|
|
string semicolonScape = Guid.NewGuid().ToString();
|
|
|
|
var escaped = clean.Replace("\\;", semicolonScape);
|
|
|
|
string[] sTokens = escaped.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
for (int buc = 0; buc < sTokens.Length; buc++)
|
|
sTokens[buc] = sTokens[buc].Trim().Replace(semicolonScape, "\\;");
|
|
|
|
List<IToken> tokens = new List<IToken>();
|
|
|
|
foreach(var token in sTokens)
|
|
{
|
|
var type = GetTokenType(token);
|
|
|
|
switch(type)
|
|
{
|
|
case TokenType.InitialValue:
|
|
tokens.Add(new InitialToken(token));
|
|
break;
|
|
case TokenType.Value:
|
|
tokens.Add(new ValueToken(token));
|
|
break;
|
|
case TokenType.Byte:
|
|
tokens.Add(new ByteToken(token));
|
|
break;
|
|
case TokenType.String:
|
|
tokens.Add(new StringToken(token));
|
|
break;
|
|
case TokenType.Group:
|
|
tokens.Add(new GroupToken(token));
|
|
break;
|
|
case TokenType.GroupName:
|
|
tokens.Add(new GroupNameToken(token));
|
|
break;
|
|
default:
|
|
throw new InvalidTokenException($"Invalid token found (\"{token}\").");
|
|
}
|
|
}
|
|
|
|
return tokens.ToArray();
|
|
|
|
}
|
|
|
|
public static TokenType GetTokenType(string Token)
|
|
{
|
|
if(initialReg.IsMatch(Token))
|
|
return TokenType.InitialValue;
|
|
|
|
if (valueReg.IsMatch(Token))
|
|
return TokenType.Value;
|
|
|
|
if (byteReg.IsMatch(Token))
|
|
return TokenType.Byte;
|
|
|
|
if (strReg.IsMatch(Token))
|
|
return TokenType.String;
|
|
|
|
if (nameReg.IsMatch(Token))
|
|
return TokenType.GroupName;
|
|
|
|
if(groupReg.IsMatch(Token))
|
|
return TokenType.Group;
|
|
|
|
return TokenType.None;
|
|
}
|
|
|
|
public static bool[] GetSamples(ValueToken Token, ref bool SignalState)
|
|
{
|
|
bool[] samples = new bool[Token.Value];
|
|
switch (Token.ValueType)
|
|
{
|
|
case ValueTokenType.High:
|
|
SignalState = true;
|
|
break;
|
|
case ValueTokenType.Low:
|
|
SignalState = false;
|
|
break;
|
|
case ValueTokenType.Equal:
|
|
break;
|
|
case ValueTokenType.Invert:
|
|
SignalState = !SignalState;
|
|
break;
|
|
default:
|
|
|
|
throw new InvalidTokenException($"Invalid value token type: {Token.ValueType}");
|
|
}
|
|
|
|
Array.Fill(samples, SignalState);
|
|
return samples;
|
|
}
|
|
|
|
public static bool[] GetSamples(ByteToken Token, IEnumerable<GroupToken> NamedGroups, ref bool SignalState)
|
|
{
|
|
return GetByteSamples(Token.Value, Token.ByteType == ByteTokenType.ByteLM, NamedGroups, ref SignalState);
|
|
}
|
|
|
|
public static bool[] GetSamples(StringToken Token, IEnumerable<GroupToken> NamedGroups, ref bool SignalState)
|
|
{
|
|
string unEscaped = UnescapeString(Token.Value);
|
|
|
|
byte[] bytes = Encoding.ASCII.GetBytes(unEscaped);
|
|
|
|
List<bool> samples = new List<bool>();
|
|
|
|
foreach (byte b in bytes)
|
|
samples.AddRange(GetByteSamples(b, Token.StringType == StringTokenType.StringLM, NamedGroups, ref SignalState));
|
|
|
|
return samples.ToArray();
|
|
}
|
|
|
|
private static string UnescapeString(string Value)
|
|
{
|
|
return Value.Replace("\\a", "\a")
|
|
.Replace("\\b", "\b")
|
|
.Replace("\\f", "\f")
|
|
.Replace("\\n", "\n")
|
|
.Replace("\\r", "\r")
|
|
.Replace("\\t", "\t")
|
|
.Replace("\\v", "\v")
|
|
.Replace("\\\\", "\\")
|
|
.Replace("\\\"", "\"")
|
|
.Replace("\\;", ";")
|
|
.Replace("\\,", ",");
|
|
}
|
|
|
|
private static bool[] GetByteSamples(byte Value, bool LSBtoMSB, IEnumerable<GroupToken> NamedGroups, ref bool SignalState)
|
|
{
|
|
List<bool> byteSamples = new List<bool>();
|
|
|
|
var zeroGroup = NamedGroups.FirstOrDefault(g => g.GroupName == "0");
|
|
var oneGroup = NamedGroups.FirstOrDefault(g => g.GroupName == "1");
|
|
|
|
if (zeroGroup == null || oneGroup == null)
|
|
throw new MissingGroupException("Found byte/string value but groups \"0\"/\"1\" are missing.");
|
|
|
|
for (int buc = 0; buc < 8; buc++)
|
|
{
|
|
if ((Value & (LSBtoMSB ? (1 << buc) : (128 >> buc))) == 0)
|
|
byteSamples.AddRange(GetSamples(zeroGroup, NamedGroups, ref SignalState));
|
|
else
|
|
byteSamples.AddRange(GetSamples(oneGroup, NamedGroups, ref SignalState));
|
|
}
|
|
|
|
return byteSamples.ToArray();
|
|
}
|
|
|
|
public static bool[] GetSamples(GroupNameToken Token, IEnumerable<GroupToken> NamedGroups, ref bool SignalState)
|
|
{
|
|
var group = NamedGroups.FirstOrDefault(g => g.GroupName == Token.Name);
|
|
|
|
if (group == null)
|
|
throw new MissingGroupException($"Cannot find a group named \"{Token.Name}\".");
|
|
|
|
return GetSamples(group, NamedGroups, ref SignalState);
|
|
}
|
|
|
|
public static bool[] GetSamples(GroupToken Group, IEnumerable<GroupToken> NamedGroups, ref bool SignalState)
|
|
{
|
|
List<bool> samples = new List<bool>();
|
|
|
|
for (int buc = 0; buc < Group.Repeats; buc++)
|
|
{
|
|
foreach (var token in Group.Tokens)
|
|
{
|
|
switch (token.TokenType)
|
|
{
|
|
case TokenType.Value:
|
|
samples.AddRange(GetSamples((ValueToken)token, ref SignalState));
|
|
break;
|
|
case TokenType.Byte:
|
|
samples.AddRange(GetSamples((ByteToken)token, NamedGroups, ref SignalState));
|
|
break;
|
|
case TokenType.String:
|
|
samples.AddRange(GetSamples((StringToken)token, NamedGroups, ref SignalState));
|
|
break;
|
|
case TokenType.GroupName:
|
|
samples.AddRange(GetSamples((GroupNameToken)token, NamedGroups, ref SignalState));
|
|
break;
|
|
default:
|
|
throw new InvalidTokenException($"Found invalid token in group: \"{token.Source}\"");
|
|
}
|
|
}
|
|
}
|
|
|
|
return samples.ToArray();
|
|
}
|
|
|
|
private static int NumberParse(string Input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Input))
|
|
throw new InvalidNumberException($"Number has an incorrect value: {Input}");
|
|
|
|
if (Input.Length > 2 && Input.Substring(0, 2).ToLower() == "0x")
|
|
{
|
|
if (Input.Length < 3)
|
|
throw new InvalidNumberException($"Number has an incorrect value: {Input}");
|
|
|
|
string hexNum = Input.Substring(2, Input.Length - 2);
|
|
int res;
|
|
|
|
if (!int.TryParse(hexNum, System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out res))
|
|
throw new InvalidNumberException($"Cannot parse hex number: {Input}");
|
|
|
|
return res;
|
|
}
|
|
else
|
|
{
|
|
int res;
|
|
|
|
if (!int.TryParse(Input, out res))
|
|
throw new InvalidNumberException($"Cannot parse decimal number: {Input}");
|
|
|
|
return res;
|
|
}
|
|
}
|
|
|
|
#region Enumerations
|
|
public enum TokenType
|
|
{
|
|
None,
|
|
InitialValue,
|
|
Group,
|
|
GroupName,
|
|
Value,
|
|
Byte,
|
|
String
|
|
}
|
|
public enum ValueTokenType
|
|
{
|
|
None,
|
|
High,
|
|
Low,
|
|
Invert,
|
|
Equal
|
|
}
|
|
public enum ByteTokenType
|
|
{
|
|
ByteLM,
|
|
ByteML
|
|
}
|
|
public enum StringTokenType
|
|
{
|
|
StringLM,
|
|
StringML
|
|
}
|
|
#endregion
|
|
|
|
#region Token definitions
|
|
public interface IToken
|
|
{
|
|
public string Source { get; }
|
|
public TokenType TokenType { get; }
|
|
}
|
|
public class InitialToken : IToken
|
|
{
|
|
public string Source { get; private set; }
|
|
public InitialToken(string Input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Input) || Input.Length < 2 || GetTokenType(Input) != TokenType.InitialValue)
|
|
throw new InvalidTokenException($"Token is not an initial token (\"{Input}\").");
|
|
|
|
int val = NumberParse(Input.Substring(1));
|
|
|
|
if (val < 0 || val > 1)
|
|
throw new InvalidTokenException("Start condition must be \"0\" or \"1\".");
|
|
|
|
Value = val == 0 ? false : true;
|
|
Source = Input;
|
|
}
|
|
public TokenType TokenType { get { return TokenType.InitialValue; } }
|
|
public bool Value { get; set; }
|
|
}
|
|
public class ValueToken : IToken
|
|
{
|
|
public string Source { get; private set; }
|
|
public ValueToken(string Input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Input) || Input.Length < 2 || GetTokenType(Input) != TokenType.Value)
|
|
throw new InvalidTokenException($"Token is not a value (\"{Input}\").");
|
|
|
|
string valType = Input.Substring(0, 1);
|
|
int val = NumberParse(Input.Substring(1));
|
|
|
|
if (val < 0)
|
|
throw new InvalidTokenException($"Value out of range (\"{Input}\")");
|
|
|
|
switch (valType)
|
|
{
|
|
case "h":
|
|
case "H":
|
|
ValueType = ValueTokenType.High;
|
|
break;
|
|
case "l":
|
|
case "L":
|
|
ValueType = ValueTokenType.Low;
|
|
break;
|
|
case "!":
|
|
ValueType = ValueTokenType.Invert;
|
|
break;
|
|
case "=":
|
|
ValueType = ValueTokenType.Equal;
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidTokenException("Token is not a value.");
|
|
|
|
}
|
|
|
|
Value = val;
|
|
Source = Input;
|
|
|
|
}
|
|
public TokenType TokenType { get { return TokenType.Value; } }
|
|
public ValueTokenType ValueType { get; set; }
|
|
public int Value { get; set; }
|
|
}
|
|
public class ByteToken : IToken
|
|
{
|
|
public string Source { get; private set; }
|
|
public ByteToken(string Input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Input) || Input.Length < 2 || GetTokenType(Input) != TokenType.Byte)
|
|
throw new InvalidTokenException($"Token is not a byte (\"{Input}\").");
|
|
|
|
string valType = Input.Substring(0, 1);
|
|
int val = NumberParse(Input.Substring(1));
|
|
if (val < 0 || val > 255)
|
|
throw new InvalidTokenException($"Byte value out of range (\"{Input}\")");
|
|
|
|
if (valType == "b")
|
|
ByteType = ByteTokenType.ByteLM;
|
|
else
|
|
ByteType = ByteTokenType.ByteML;
|
|
|
|
Value = (byte)val;
|
|
Source = Input;
|
|
|
|
}
|
|
public TokenType TokenType { get { return TokenType.Byte; } }
|
|
public ByteTokenType ByteType { get; set; }
|
|
public byte Value { get; set; }
|
|
}
|
|
public class StringToken : IToken
|
|
{
|
|
public string Source { get; private set; }
|
|
public StringToken(string Input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Input) || Input.Length < 4 || GetTokenType(Input) != TokenType.String)
|
|
throw new InvalidTokenException($"Token is not a string (\"{Input}\").");
|
|
|
|
string valType = Input.Substring(0, 1);
|
|
string val = Input.Substring(2,Input.Length - 3);
|
|
|
|
if (valType == "s")
|
|
StringType = StringTokenType.StringLM;
|
|
else
|
|
StringType = StringTokenType.StringML;
|
|
|
|
Value = val;
|
|
Source = Input;
|
|
|
|
}
|
|
public TokenType TokenType { get { return TokenType.String; } }
|
|
public StringTokenType StringType { get; set; }
|
|
public string Value { get; set; }
|
|
}
|
|
public class GroupNameToken : IToken
|
|
{
|
|
public string Source { get; private set; }
|
|
public GroupNameToken(string Input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Input) || Input.Length < 3 || GetTokenType(Input) != TokenType.GroupName)
|
|
throw new InvalidTokenException($"Token is not a group name (\"{Input}\").");
|
|
|
|
Name = Input.Substring(1, Input.Length - 2);
|
|
Source = Input;
|
|
}
|
|
public TokenType TokenType { get { return TokenType.GroupName; } }
|
|
public string Name { get; set; }
|
|
}
|
|
public class GroupToken : IToken
|
|
{
|
|
public string Source { get; private set; }
|
|
public GroupToken(string Input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Input) || Input.Length < 3 || GetTokenType(Input) != TokenType.Group)
|
|
throw new InvalidTokenException($"Token is not a group (\"{Input}\").");
|
|
|
|
var match = groupReg.Match(Input);
|
|
|
|
string commaScape = Guid.NewGuid().ToString();
|
|
string escaped = match.Groups[3].Value.Replace("\\,", commaScape);
|
|
|
|
string[] internalTokens = escaped.Replace("}", "").Split(',');
|
|
|
|
for(int buc = 0; buc < internalTokens.Length;buc++)
|
|
internalTokens[buc] = internalTokens[buc].Trim().Replace(commaScape, "\\,");
|
|
|
|
if (!string.IsNullOrWhiteSpace(match.Groups[2].Value))
|
|
GroupName = match.Groups[2].Value.Replace("<", "").Replace(">", "").Replace(",", "");
|
|
|
|
List<IToken> tokens = new List<IToken>();
|
|
|
|
foreach (var token in internalTokens)
|
|
{
|
|
switch (GetTokenType(token))
|
|
{
|
|
case TokenType.Value:
|
|
tokens.Add(new ValueToken(token));
|
|
break;
|
|
case TokenType.Byte:
|
|
tokens.Add(new ByteToken(token));
|
|
break;
|
|
case TokenType.String:
|
|
tokens.Add(new StringToken(token));
|
|
break;
|
|
case TokenType.GroupName:
|
|
tokens.Add(new GroupNameToken(token));
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidTokenException($"Invalid token in group (\"{token}\").");
|
|
}
|
|
}
|
|
|
|
if(tokens.Count == 0)
|
|
throw new InvalidGroupException($"Group contains no tokens (\"{Input}\").");
|
|
|
|
Repeats = int.Parse(match.Groups[13].Value);
|
|
Tokens = tokens.ToArray();
|
|
Source = Input;
|
|
}
|
|
public TokenType TokenType { get { return TokenType.Group; } }
|
|
public string? GroupName { get; set; }
|
|
public IToken[] Tokens { get; set; }
|
|
public int Repeats { get; set; }
|
|
}
|
|
#endregion
|
|
/// <summary>
|
|
/// Tokenized SDL class, used to parse and convert a text source to tokens and samples
|
|
/// </summary>
|
|
public class TokenizedSDL
|
|
{
|
|
string source;
|
|
IToken[] tokens;
|
|
public string Source { get { return source; } }
|
|
public IToken[] Tokens
|
|
{
|
|
get
|
|
{
|
|
return tokens;
|
|
}
|
|
}
|
|
public bool? InitialValue
|
|
{
|
|
get
|
|
{
|
|
return (tokens.FirstOrDefault(t => t.TokenType == TokenType.InitialValue)
|
|
as InitialToken)?.Value;
|
|
}
|
|
}
|
|
public ValueToken[] Values
|
|
{
|
|
get
|
|
{
|
|
return tokens.Where(t => t.TokenType == TokenType.Value)
|
|
.Cast<ValueToken>().ToArray();
|
|
}
|
|
}
|
|
public ByteToken[] Bytes
|
|
{
|
|
get
|
|
{
|
|
return tokens.Where(t => t.TokenType == TokenType.Byte)
|
|
.Cast<ByteToken>().ToArray();
|
|
}
|
|
}
|
|
public StringToken[] Strings
|
|
{
|
|
get
|
|
{
|
|
return tokens.Where(t => t.TokenType == TokenType.String)
|
|
.Cast<StringToken>().ToArray();
|
|
}
|
|
}
|
|
public GroupNameToken[] GroupNames
|
|
{
|
|
get
|
|
{
|
|
return tokens.Where(t => t.TokenType == TokenType.GroupName)
|
|
.Cast<GroupNameToken>().ToArray();
|
|
}
|
|
}
|
|
public GroupToken[] Groups
|
|
{
|
|
get
|
|
{
|
|
return tokens.Where(t => t.TokenType == TokenType.Group)
|
|
.Cast<GroupToken>().ToArray();
|
|
}
|
|
}
|
|
public GroupToken[] NamedGroups
|
|
{
|
|
get
|
|
{
|
|
return tokens.Where(t => t.TokenType == TokenType.Group &&
|
|
(!string.IsNullOrWhiteSpace((t as GroupToken).GroupName)))
|
|
.Cast<GroupToken>().ToArray();
|
|
}
|
|
}
|
|
public TokenizedSDL(string Input)
|
|
{
|
|
source = Input;
|
|
tokens = GetTokens(Input);
|
|
|
|
var values = Values;
|
|
var names = GroupNames;
|
|
var groups = Groups;
|
|
var namedGroups = NamedGroups;
|
|
|
|
var missingNames = names.Where(n => !namedGroups.Any(ng => ng.GroupName == n.Name)).Select(n => n.Name).ToArray();
|
|
var missingNamesInGroups = groups.SelectMany(g => g.Tokens
|
|
.Where(t => t.TokenType == TokenType.GroupName)
|
|
.Cast<GroupNameToken>()
|
|
.Where(n => !namedGroups.Any(ng => ng.GroupName == n.Name))
|
|
.Select(n => n.Name))
|
|
.ToArray();
|
|
|
|
List<string> missing = new List<string>();
|
|
if (missingNames != null)
|
|
missing.AddRange(missingNames);
|
|
if (missingNamesInGroups != null)
|
|
missing.AddRange(missingNamesInGroups);
|
|
|
|
if (missing.Count > 0)
|
|
{
|
|
string missingString = string.Join(", ", missing.Distinct());
|
|
throw new MissingGroupException($"Missing named groups: {missingString}");
|
|
}
|
|
|
|
var needsBitGroups = (Bytes != null && Bytes.Length > 0) || (Strings != null && Strings.Length > 0);
|
|
|
|
if (needsBitGroups)
|
|
{
|
|
if (!NamedGroups.Any(g => g.GroupName == "0") || !NamedGroups.Any(g => g.GroupName == "1"))
|
|
throw new MissingGroupException($"Definition contains bytes/strings but groups \"0\"/\"1\" are not defined.");
|
|
}
|
|
|
|
var duplicatedNames = namedGroups.GroupBy(g => g.GroupName)
|
|
.Where(gg => gg.Count() > 1)
|
|
.Select(gg => gg.Key)
|
|
.ToArray();
|
|
|
|
if (duplicatedNames != null && duplicatedNames.Length > 0)
|
|
throw new DuplicatedGroupException($"Found duplicated named groups: {string.Join(", ", duplicatedNames)}");
|
|
}
|
|
public bool[] ToSamples(bool? InitialValue = null)
|
|
{
|
|
bool currentState = InitialValue ?? (this.InitialValue ?? false);
|
|
var namedGroups = NamedGroups;
|
|
List<bool> samples = new List<bool>();
|
|
|
|
foreach (var token in Tokens)
|
|
{
|
|
switch(token.TokenType)
|
|
{
|
|
case TokenType.Value:
|
|
samples.AddRange(GetSamples((ValueToken)token, ref currentState));
|
|
break;
|
|
case TokenType.Byte:
|
|
samples.AddRange(GetSamples((ByteToken)token, namedGroups, ref currentState));
|
|
break;
|
|
case TokenType.String:
|
|
samples.AddRange(GetSamples((StringToken)token, namedGroups, ref currentState));
|
|
break;
|
|
case TokenType.GroupName:
|
|
samples.AddRange(GetSamples((GroupNameToken)token, namedGroups, ref currentState));
|
|
break;
|
|
case TokenType.Group:
|
|
|
|
var group = (GroupToken)token;
|
|
|
|
if (!string.IsNullOrWhiteSpace(group.GroupName))
|
|
continue;
|
|
|
|
samples.AddRange(GetSamples(group, namedGroups, ref currentState));
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
return samples.ToArray();
|
|
}
|
|
}
|
|
|
|
#region Exceptions
|
|
public class MissingGroupException : Exception
|
|
{
|
|
public MissingGroupException(string message) : base(message) { }
|
|
}
|
|
public class DuplicatedGroupException : Exception
|
|
{
|
|
public DuplicatedGroupException(string message) : base(message) { }
|
|
}
|
|
public class InvalidTokenException : Exception
|
|
{
|
|
public InvalidTokenException(string message) : base(message) { }
|
|
}
|
|
public class InvalidGroupException : Exception
|
|
{
|
|
public InvalidGroupException(string message) : base(message) { }
|
|
}
|
|
public class InvalidNumberException : Exception
|
|
{
|
|
public InvalidNumberException(string message) : base(message) { }
|
|
}
|
|
#endregion
|
|
}
|
|
}
|