using System; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using Robust.Shared.Reflection; namespace Robust.Shared.ViewVariables; internal abstract partial class ViewVariablesManager { private static readonly Regex IndexerRegex = new (@"\[[^\[]+\]", RegexOptions.Compiled); private static readonly Regex TypeSpecifierRegex = new (@"\{[^\{]+\}", RegexOptions.Compiled); public ViewVariablesPath? ResolvePath(string path) { if (string.IsNullOrEmpty(path)) return null; if (path.StartsWith('/')) path = path[1..]; if (path.EndsWith('/')) path = path[..^1]; var segments = path.Split('/'); // Technically, this should never happen... But hey, better be safe than sorry? if (segments.Length == 0) return null; var domain = segments[0]; if (!_registeredDomains.TryGetValue(domain, out var domainData)) return null; var (newPath, relativePath) = domainData.ResolveObject(string.Join('/', segments[1..])); return ResolveRelativePath(newPath, relativePath); } private ViewVariablesPath? ResolveRelativePath(ViewVariablesPath? path, string[] segments) { ViewVariablesComponentPath? oldComp; // Who needs recursion, am I right? while (true) { if (path is ViewVariablesComponentPath compPath) path.ParentComponent = compPath; oldComp = path?.ParentComponent; // Empty path, return current path. This can happen as we slowly take away segments from the array. if (segments.Length == 0) { return path; } if (path?.Get() is not {} obj) return null; var nextSegment = segments[0]; if (string.IsNullOrEmpty(nextSegment)) { // Let's ignore that... segments = segments[1..]; continue; } var specifiers = TypeSpecifierRegex.Matches(nextSegment); var indexers = IndexerRegex.Matches(nextSegment); var nextSegmentClean = TypeSpecifierRegex.Replace( IndexerRegex.Replace(nextSegment, string.Empty), string.Empty); // Yeah, that's not valid bud. if (specifiers.Count > 1) return null; VVAccess? access = null; if (specifiers.Count == 1 || ResolveTypeHandlers(path, nextSegmentClean) is not {} customPath) { Type? declaringType = null; if (specifiers.Count == 1 && _reflectionMan.GetType(specifiers[0].Value[1..^1]) is {} t) { declaringType = t; } var memberInfo = obj.GetType().GetSingleMember(nextSegmentClean, declaringType); if (memberInfo == null || !ViewVariablesUtility.TryGetViewVariablesAccess(memberInfo, out access)) return null; path = memberInfo switch { FieldInfo or PropertyInfo => new ViewVariablesFieldOrPropertyPath(obj, memberInfo, _entMan), MethodInfo methodInfo => new ViewVariablesMethodPath(obj, methodInfo), _ => throw new InvalidOperationException("Invalid member! Must be a property, field or method.") }; } else { path = customPath; access = VVAccess.ReadWrite; } UpdateParentComp(path, ref oldComp); // After this, obj is essentially the parent. foreach (Match match in indexers) { path = ResolveIndexing(path, ParseArguments(match.Value[1..^1]), access.Value); UpdateParentComp(path, ref oldComp); } segments = segments[1..]; } } private void UpdateParentComp(ViewVariablesPath? newPath, ref ViewVariablesComponentPath? oldPath) { if (newPath == null) return; if (newPath is ViewVariablesComponentPath newCompPath) newPath.ParentComponent = newCompPath; else if (newPath != null) newPath.ParentComponent ??= oldPath; oldPath = newPath?.ParentComponent; } private ViewVariablesPath? ResolveIndexing(ViewVariablesPath? path, string[] arguments, VVAccess access) { if (path?.Get() is not {} obj || arguments.Length == 0) return null; var type = obj.GetType(); // Multidimensional arrays... More like, painful arrays. if (type.IsArray && type.GetArrayRank() > 1) { var getter = type.GetSingleMember("Get") as MethodInfo; var setter = type.GetSingleMember("Set") as MethodInfo; if (getter == null && setter == null) return null; var p = DeserializeArguments( getter?.GetParameters().Select(p => p.ParameterType).ToArray() ?? setter!.GetParameters()[1..].Select(p => p.ParameterType).ToArray(), 0, arguments); object? Get() { return getter?.Invoke(obj, p); } void Set(object? value) { if(p != null && access == VVAccess.ReadWrite) setter?.Invoke(obj, new[] {value}.Concat(p).ToArray()); } return new ViewVariablesFakePath(Get, Set, null, getter?.ReturnType ?? setter!.GetParameters()[0].ParameterType); } // No indexer. if (type.GetIndexer() is not {} indexer) return null; var parametersInfo = indexer.GetIndexParameters(); var parameters = DeserializeArguments( parametersInfo.Select(p => p.ParameterType).ToArray(), parametersInfo.Count(p => p.IsOptional), arguments); if (parameters == null) return null; return new ViewVariablesIndexedPath(obj, indexer, parameters, access); } private ViewVariablesPath? ResolveTypeHandlers(ViewVariablesPath path, string relativePath) { if (path.Get() is not {} obj || string.IsNullOrEmpty(relativePath) || relativePath.Contains('/')) return null; foreach (var handler in GetAllTypeHandlers(obj.GetType())) { if (handler.HandlePath(path, relativePath) is {} newPath) return newPath; } // Not handled by a custom type handler! return null; } }