Beispiel #1
0
        /// <summary>
        /// Gets the <see cref="TargetFramework"/> for the specified <paramref name="module"/>.
        /// </summary>
        /// <param name="module">The module to get the target framework description for. Cannot be null.</param>
        /// <returns>A new instance of the <see cref="TargetFramework"/> class that describes the specified <paramref name="module"/>.
        /// </returns>
        public static TargetFramework DetectTargetFramework(PEFile module)
        {
            if (module is null)
            {
                throw new ArgumentNullException(nameof(module));
            }

            int versionNumber;

            switch (module.GetRuntime())
            {
            case TargetRuntime.Net_1_0:
                versionNumber = 100;
                break;

            case TargetRuntime.Net_1_1:
                versionNumber = 110;
                break;

            case TargetRuntime.Net_2_0:
                versionNumber = 200;
                // TODO: Detect when .NET 3.0/3.5 is required
                break;

            default:
                versionNumber = 400;
                break;
            }

            string targetFrameworkIdentifier = null;
            string targetFrameworkProfile    = null;

            string targetFramework = module.DetectTargetFrameworkId();

            if (!string.IsNullOrEmpty(targetFramework))
            {
                string[] frameworkParts = targetFramework.Split(',');
                targetFrameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase) && !a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase));
                string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase));

                if (frameworkVersion != null)
                {
                    versionNumber = int.Parse(frameworkVersion.Substring(VersionToken.Length + 1).Replace(".", ""));
                    if (versionNumber < 100)
                    {
                        versionNumber *= 10;
                    }
                }

                string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase));
                if (frameworkProfile != null)
                {
                    targetFrameworkProfile = frameworkProfile.Substring(ProfileToken.Length);
                }
            }

            return(new TargetFramework(targetFrameworkIdentifier, versionNumber, targetFrameworkProfile));
        }
Beispiel #2
0
        private TargetFramework DetectTargetFramework(PEFile module)
        {
            TargetFramework result = default;

            switch (module.GetRuntime())
            {
            case Metadata.TargetRuntime.Net_1_0:
                result.VersionNumber          = 100;
                result.TargetFrameworkVersion = "v1.0";
                break;

            case Metadata.TargetRuntime.Net_1_1:
                result.VersionNumber          = 110;
                result.TargetFrameworkVersion = "v1.1";
                break;

            case Metadata.TargetRuntime.Net_2_0:
                result.VersionNumber          = 200;
                result.TargetFrameworkVersion = "v2.0";
                // TODO: Detect when .NET 3.0/3.5 is required
                break;

            default:
                result.VersionNumber          = 400;
                result.TargetFrameworkVersion = "v4.0";
                break;
            }

            string targetFramework = module.DetectTargetFrameworkId();

            if (!string.IsNullOrEmpty(targetFramework))
            {
                string[] frameworkParts = targetFramework.Split(',');
                result.TargetFrameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith("Version=", StringComparison.OrdinalIgnoreCase) && !a.StartsWith("Profile=", StringComparison.OrdinalIgnoreCase));
                string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith("Version=", StringComparison.OrdinalIgnoreCase));
                if (frameworkVersion != null)
                {
                    result.TargetFrameworkVersion = frameworkVersion.Substring("Version=".Length);
                    result.VersionNumber          = int.Parse(frameworkVersion.Substring("Version=v".Length).Replace(".", ""));
                    if (result.VersionNumber < 100)
                    {
                        result.VersionNumber *= 10;
                    }
                }
                string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith("Profile=", StringComparison.OrdinalIgnoreCase));
                if (frameworkProfile != null)
                {
                    result.TargetFrameworkProfile = frameworkProfile.Substring("Profile=".Length);
                }
            }
            return(result);
        }
Beispiel #3
0
        public CodeDecompiler(string assemblyName, Stream stream)
        {
            // Necessary to create the decompiler.
            var settings = new DecompilerSettings();

            var file = new PEFile(assemblyName, stream);

            // Creates instance of CSharpDecompiler.
            _decompiler = new CSharpDecompiler(
                file,
                new UniversalAssemblyResolver(
                    assemblyName,
                    settings.ThrowOnAssemblyResolveErrors,
                    file.DetectTargetFrameworkId(),
                    null,
                    settings.LoadInMemory ? PEStreamOptions.PrefetchMetadata : PEStreamOptions.Default,
                    settings.ApplyWindowsRuntimeProjections
                        ? MetadataReaderOptions.ApplyWindowsRuntimeProjections
                        : MetadataReaderOptions.None
                    ),
                settings
                );
        }
Beispiel #4
0
		static void Main(string[] args) {
			// args[0] is the path to Opus Magnum EXE
			string exe = args[0];
			// args[1] is the path to mappings CSV
			string mappingsLoc = args[1];
			// if there's a third string, its the path out.csv
			if(args.Length > 2) {
				Console.WriteLine("Reading strings...");
				string[] lines = File.ReadAllLines(args[2]);
				bool hadSplit = true; // multi-line strings
				int lastIndex = 0;
				foreach(string line in lines) {
					string[] split = line.Split("~,~");
					if(split.Length > 1) {
						// if we *can* split on this line, then we're definitely at the first line of a string
						hadSplit = true;
						try {
							lastIndex = int.Parse(split[0]);
							Strings.Add(lastIndex, split[1]);
						} catch(ArgumentException) { }
					} else if(!hadSplit) {
						// if this line isn't blank (or even if it is), then we're continuing a previous multi-line string, so append
						Strings[lastIndex] = Strings[lastIndex] + "\n" + line;
					}
				}
				// these are ridden with special characters
				// we can't just trim normally, see "fmt " breaking WAV loading
				// so we manually regex replace: [^a-zA-Z0-9_.:\n;'*()+<>\\{}# ,~/$\[\]\-©!"?&’\t=—@%●●●●…—……] is removed
				// this kills other languages, a better solution is needed in the future
				foreach(int key in Strings.Keys.ToList())
					Strings[key] = Regex.Replace(Strings[key], "[^a-zA-Z0-9_.:\n;'*()+<>\\\\{}# ,~/$\\[\\]\\-©!\" ? &’\t =—@%●●●●…—……]", "");
			}

			Console.WriteLine("Reading mappings...");
			// for every line that doesn't start with a "#", split by "," and add to mappings
			string[] mappingsFile = File.ReadAllLines(mappingsLoc);
			foreach(var line in mappingsFile){
				if(!line.StartsWith("#") && !line.Trim().Equals("")){
					string[] split = line.Split(",", 2);
					mappings.Add(split[0], split[1]);
				}
			}

			var module = new PEFile(exe);
			var decompiler = new CSharpDecompiler(exe, new UniversalAssemblyResolver(exe, false, module.DetectTargetFrameworkId()), new DecompilerSettings() {
				NamedArguments = false
			});

			// decompile
			Console.WriteLine("Decompiling...");
			var ast = decompiler.DecompileWholeModuleAsSingleFile();

			// we now have a syntax tree
			// we just need to walk it, modify class and member name references, and then output to files
			Console.WriteLine("Collecting intermediary names...");
			ast.AcceptVisitor(new IdentifierCollectingVisitor());

			Console.WriteLine("Remapping...");
			ast.AcceptVisitor(new RemappingVisitor());

			Console.WriteLine("Cleaning up invalid code...");
			ast.AcceptVisitor(new CleanupVisitor());

			// some params are in the wrong place...

			//Console.WriteLine("Adding modded entry point...");
			//ast.AcceptVisitor(new EntrypointAddingVisitor());

			Console.WriteLine("Writing nonsense -> intermediary...");
			using StreamWriter intermediaryFile = new StreamWriter("./intermediary.txt");
			foreach(KeyValuePair<string, string> kv in IdentifierCollectingVisitor.intermediary) {
				intermediaryFile.WriteLine(kv.Key + " -> " + kv.Value);
			}
			foreach(KeyValuePair<KeyValuePair<string, string>, string> kv in IdentifierCollectingVisitor.paramIntermediary) {
				intermediaryFile.WriteLine(kv.Key.Key + ", " + kv.Key.Value + " -> " + kv.Value);
			}

			string code = ast.ToString();

			// if there's a fourth string, its the path to patch.diff
			// apply patch and compile
			if(args.Length > 3) {
				Console.WriteLine("Applying compilation patch...");
				string patchFile = File.ReadAllText(args[3]);
				var diff = DiffParserHelper.Parse(patchFile);
				code = PatchHelper.Patch(code, diff.First().Chunks, "\n");

				Console.WriteLine("Recompiling...");
				SyntaxTree syntax = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions());
				var options = new CSharpCompilationOptions(
					OutputKind.WindowsApplication,
					optimizationLevel: OptimizationLevel.Release,
					allowUnsafe: true,
					warningLevel: 1,
					platform: Platform.X86
				);
				// File/Process/Directory doesn't exist
				var compilation = CSharpCompilation.Create("ModdedLightning", options: options)
					.AddReferences(new MetadataReference[]{
						// add libs
						MetadataReference.CreateFromFile(typeof(string).Assembly.Location),
						MetadataReference.CreateFromFile(typeof(HashSet<object>).Assembly.Location),
						MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
						MetadataReference.CreateFromFile(Assembly.Load("mscorlib").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.Runtime.Extensions").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.IO").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.IO.FileSystem").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.IO.FileSystem.Watcher").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.Net.Requests").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.Diagnostics.Process").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.Private.Uri").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.ComponentModel.Primitives").Location),
						MetadataReference.CreateFromFile(Assembly.Load("System.Console").Location),
						MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location),
						MetadataReference.CreateFromFile(typeof(Steamworks.CSteamID).Assembly.Location),
						MetadataReference.CreateFromFile(typeof(Ionic.Zip.ZipEntry).Assembly.Location)
					})
					.AddSyntaxTrees(syntax);
				using FileStream outputAssembly = new FileStream("./ModdedLightning.exe", FileMode.Create);
				var res = compilation.Emit(outputAssembly);
				if(res.Success) {
					Console.WriteLine("Successfully recompiled!");
					Console.WriteLine("(Press any key to continue.)");
					Console.ReadKey();
					Console.WriteLine("Writing runtime config & running batch...");

					string runtimeConfig = @"{ ""runtimeOptions"": { ""tfm"": ""netcoreapp3.1"", ""framework"": { ""name"": ""Microsoft.NETCore.App"", ""version"": ""3.1.0"" }}}";
					using StreamWriter configFile = new StreamWriter("./ModdedLightning.runtimeconfig.json");
					configFile.WriteLine(runtimeConfig);

					string runBatch = @"""C:\Program Files (x86)\dotnet\dotnet.exe"" ModdedLightning.exe";
					using StreamWriter batchFile = new StreamWriter("./runModded.bat");
					batchFile.WriteLine(runBatch);
				} else {
					Console.WriteLine("Recompilation failed with " + res.Diagnostics.Length + " errors.");
					foreach(var error in res.Diagnostics) {
						Console.WriteLine("Location: " + error.Location.GetLineSpan());
						Console.WriteLine("    " + error.GetMessage());
						Console.WriteLine("(Press any key to continue.)");
						Console.ReadKey();
					}
				}
			}

			Console.WriteLine("Writing code output...");
			using StreamWriter outputFile = new StreamWriter("./decomp.cs");
			outputFile.WriteLine(code);

			Console.WriteLine("Done!");
		}
Beispiel #5
0
        /// <summary>
        /// Saves this object as an Inno Setup formatted script
        /// </summary>
        /// <param name="textWriter"><see cref="TextWriter"/> with which script is written</param>
        public virtual void Save(TextWriter textWriter)
        {
            var thisType    = typeof(Installation);
            var derivedType = GetType();

            var entriesBuilder = new ParameterizedEntriesBuilder(textWriter);

            // list of methods referenced by constants
            var constReferencedMethods = new HashSet <InstallationMethod>();

            _constantReferencedMethod = methodInfo => constReferencedMethods.Add(new InstallationMethod(methodInfo));

            try
            {
                using (new Section(textWriter, "Setup"))
                {
                    var setupProperties = thisType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                          .Where(p => p.GetCustomAttribute <SetupDirectiveAttribute>() != null)
                                          .Select(p =>
                                                  new
                    {
                        Alias         = p.GetCustomAttribute <AliasAttribute>()?.Name,
                        Property      = derivedType.GetProperty(p.Name),
                        TypeConverter = p.GetCustomAttribute <TypeConverterAttribute>()
                    })
                                          .Where(p => p.Property.DeclaringType != thisType)
                                          .Select(p =>
                                                  new
                    {
                        p.Property,
                        Name = p.Alias ?? p.Property.Name,
                        p.TypeConverter
                    })
                                          .OrderBy(p => p.Name)
                                          .ToList();

                    WriteDirectives(textWriter, setupProperties.Select(p => (p.Name, p.Property, p.TypeConverter)));
                }

                var langOptionsProperties = thisType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                            .Where(p => p.GetCustomAttribute <LanguageDirectiveAttribute>() != null)
                                            .Select(p =>
                                                    new
                {
                    Alias         = p.GetCustomAttribute <AliasAttribute>()?.Name,
                    Property      = derivedType.GetProperty(p.Name),
                    TypeConverter = p.GetCustomAttribute <TypeConverterAttribute>()
                })
                                            .Where(p => derivedType.GetProperty(p.Property.Name).DeclaringType != thisType)
                                            .Select(p =>
                                                    new
                {
                    p.Property,
                    Name = p.Alias ?? p.Property.Name,
                    p.TypeConverter
                })
                                            .OrderBy(p => p.Name)
                                            .ToList();

                if (langOptionsProperties.Count > 0)
                {
                    using (new Section(textWriter, "LanguageOptions"))
                    {
                        WriteDirectives(textWriter, langOptionsProperties.Select(p => (p.Name, p.Property, p.TypeConverter)));
                    }
                }

                ParameterizedEntriesBuilderHandler?.Invoke(entriesBuilder);
            }
            finally
            {
                _constantReferencedMethod = null;
            }

            using (new Section(textWriter, "Code"))
            {
                textWriter.WriteLine("const");
                textWriter.WriteLine("   MB_ICONWARNING = $30;");
                textWriter.WriteLine("   MB_ICONINFORMATION = $40;");
                textWriter.WriteLine("   MB_ICONQUESTION = $20;");
                textWriter.WriteLine("   MB_ICONERROR = $10;");
                textWriter.WriteBlankLine();

                var decompilers = new Dictionary <string, CSharpDecompiler>();

                SyntaxTree GetSyntaxTree(string assemblyLocation, int metadataToken)
                {
                    var settings = new DecompilerSettings
                    {
                        UseDebugSymbols   = true,
                        NamedArguments    = false,
                        SwitchExpressions = false,
                        OutVariables      = false,
                        SeparateLocalVariableDeclarations = true,
                        LoadInMemory         = true,
                        ShowXmlDocumentation = false,
                        Discards             = false
                    };

                    var assemblyName = assemblyLocation;

                    var peFile = new PEFile(
                        assemblyName,
                        new FileStream(assemblyName, FileMode.Open, FileAccess.Read),
                        streamOptions: settings.LoadInMemory ? PEStreamOptions.PrefetchEntireImage : PEStreamOptions.Default,
                        metadataOptions: settings.ApplyWindowsRuntimeProjections ? MetadataReaderOptions.ApplyWindowsRuntimeProjections : MetadataReaderOptions.None
                        );

                    var resolver = new UniversalAssemblyResolver(assemblyName, settings.ThrowOnAssemblyResolveErrors,
                                                                 peFile.DetectTargetFrameworkId(),
                                                                 settings.LoadInMemory ? PEStreamOptions.PrefetchMetadata : PEStreamOptions.Default,
                                                                 settings.ApplyWindowsRuntimeProjections ? MetadataReaderOptions.ApplyWindowsRuntimeProjections : MetadataReaderOptions.None);

                    var typeSystem = new DecompilerTypeSystem(peFile, resolver);

                    var decompiler = decompilers.Set(
                        assemblyName,
                        new CSharpDecompiler(typeSystem, settings)
                    {
                        DebugInfoProvider = new MetadataDebugInfoProvider(assemblyName)
                    }
                        );
                    var method = MetadataTokenHelpers.TryAsEntityHandle(metadataToken);

                    return(decompiler.Decompile(new List <EntityHandle>()
                    {
                        method.Value
                    }));
                }

                var eventHandlers = thisType.GetMethods(BindingFlags.Instance | BindingFlags.Public).Where(m => m.GetCustomAttribute <EventHandlerAttribute>() != null)
                                    .Select(m => new
                {
                    DerivedMethod   = derivedType.GetMethod(m.Name, BindingFlags.Public | BindingFlags.Instance),
                    InterfaceMethod = m
                })
                                    .Select(m =>
                {
                    if (m.DerivedMethod.DeclaringType != typeof(Installation))
                    {
                        return(new InstallationMethod(m.DerivedMethod, m.InterfaceMethod));
                    }

                    return(null);
                })
                                    .Where(m => m != null)
                                    .ToList();

                var allMethods = entriesBuilder.Methods
                                 .Concat(eventHandlers)
                                 .Concat(constReferencedMethods)
                                 .ToList();

                var aliasFactory = new Func <string, string>(name =>
                {
                    var method = allMethods.Single(m => m.Name == name);
                    return(method.GetAttribute <AliasAttribute>()?.Name);
                });

                bool encounteredInitializeSetup     = false;
                bool encounteredInitializeUninstall = false;
                var  referencedGlobalVariables      = new Dictionary <FieldInfo, string>();
                var  definedMethods  = new HashSet <string>();
                var  declaredMembers = new HashSet <MemberInfo>();
                var  namespaces      = new HashSet <string>();
                using var typeDefinitions = new Snippet();

                var type = derivedType;
                while (type != null && type != typeof(Installation))
                {
                    namespaces.Add(type.Namespace);
                    type = type.BaseType;
                }

                using (var snippet = new Snippet())
                {
                    foreach (var installationMethod in allMethods)
                    {
                        switch (installationMethod.Name)
                        {
                        case nameof(Installation.InitializeSetup):
                            encounteredInitializeSetup = true;
                            break;

                        case nameof(Installation.InitializeUninstall):
                            encounteredInitializeUninstall = true;
                            break;
                        }

                        if (!definedMethods.Add(installationMethod.UniqueId))
                        {
                            // already defined, probably via recursion when one method called another
                            continue;
                        }

                        var ast = GetSyntaxTree(installationMethod.AssemblyLocation, installationMethod.MetadataToken);

                        //ast.VisitChildren(new DiagnosticVisitor(Console.Out));

                        var methodDecl = ast.Children.OfType <MethodDeclaration>().Single();

                        using (var masterWriter = new TextCodeWriter(snippet, null, true))
                        {
                            var codeWriterFactory = new NestedCodeWriterFactory(masterWriter);

                            using (var methodWriter = codeWriterFactory.New())
                            {
                                var context = new PascalScriptVisitorContext(
                                    this,
                                    methodWriter,
                                    null,
                                    namespaces,
                                    mi => GetSyntaxTree(mi.DeclaringType.Assembly.Location, mi.MetadataToken),
                                    referencedGlobalVariables,
                                    aliasFactory,
                                    new TextCodeWriter(typeDefinitions, null, false),
                                    declaredMembers,
                                    () => codeWriterFactory.New(),
                                    derivedType.BaseType,
                                    definedMethods);

                                ast.VisitChildren(new PascalScriptVisitor(context));

                                methodWriter.WriteBlankLine();
                            }
                        }
                    }

                    using (var codeWriter = new TextCodeWriter(textWriter, null, true))
                    {
                        typeDefinitions.CopyTo(codeWriter);

                        var usedSwitches           = new HashSet <string>();
                        var variableInitialization = new List <(string name, string rhs)>();

                        // write global variables
                        if (referencedGlobalVariables.Count > 0)
                        {
                            codeWriter.WriteLine("var");

                            using (codeWriter.Indent())
                            {
                                foreach (var globalVariable in referencedGlobalVariables)
                                {
                                    codeWriter.WriteLine($"{globalVariable.Value}: {globalVariable.Key.FieldType.ToPascal()};");

                                    var defaultValue          = globalVariable.Key.GetValue(this);
                                    var formattedDefaultValue = defaultValue == null || globalVariable.Key.FieldType.IsStruct() ? null : PascalScriptVisitor.FormatPrimitive(defaultValue);
                                    var cmdLineAttr           = (globalVariable.Key.GetCustomAttribute <CommandLineParameterAttribute>());

                                    if (cmdLineAttr != null)
                                    {
                                        var name           = cmdLineAttr.SwitchName ?? globalVariable.Key.Name;
                                        var initialization = $"ExpandConstant('{{param:{name.ToLower()}";

                                        if (defaultValue != null)
                                        {
                                            defaultValue = defaultValue.GetType() == typeof(bool) ?
                                                           ((bool)defaultValue ? "1" : "0") :
                                                           formattedDefaultValue;

                                            initialization += $"|{defaultValue}";
                                        }

                                        initialization += "}')";

                                        if (globalVariable.Key.FieldType == typeof(bool))
                                        {
                                            initialization = $"({initialization} = '1')";
                                        }

                                        if (!usedSwitches.Add(name))
                                        {
                                            throw new NotSupportedException($"The command line parameter name {name} can be used only once");
                                        }

                                        variableInitialization.Add((globalVariable.Key.Name, initialization));
                                    }
                                    else if (formattedDefaultValue != null)
                                    {
                                        // FormatPrimitive() won't single quote the string
                                        if (defaultValue is string)
                                        {
                                            formattedDefaultValue = $"'{formattedDefaultValue}'";
                                        }

                                        variableInitialization.Add((globalVariable.Key.Name, formattedDefaultValue));
                                    }
                                }
                            }
                        }

                        codeWriter.WriteBlankLine();

                        // write body of code
                        snippet.CopyTo(codeWriter);

                        using (var globalVariableSnippet = new Snippet())
                        {
                            using (var globalVariableWriter = new TextCodeWriter(globalVariableSnippet, null, true))
                            {
                                variableInitialization.ForEach(init => globalVariableWriter.WriteLine($"{init.name} := {init.rhs};"));
                            }

                            if (encounteredInitializeSetup || variableInitialization.Count > 0)
                            {
                                // write InitializeSetup
                                codeWriter.WriteLine($"function {nameof(Installation.InitializeSetup)}: Boolean;");
                                codeWriter.WriteBegin();

                                using (codeWriter.Indent())
                                {
                                    // write variable initialization
                                    globalVariableSnippet.CopyTo(codeWriter);

                                    codeWriter.WriteBlankLine();
                                    codeWriter.WriteLine(encounteredInitializeSetup ? "Result := this_InitializeSetup();" : "Result := True;");
                                }

                                codeWriter.WriteEnd();
                            }

                            if (encounteredInitializeUninstall || variableInitialization.Count > 0)
                            {
                                // write InitializeSetup
                                codeWriter.WriteLine($"function {nameof(Installation.InitializeUninstall)}: Boolean;");
                                codeWriter.WriteBegin();

                                using (codeWriter.Indent())
                                {
                                    // write variable initialization
                                    globalVariableSnippet.CopyTo(codeWriter);

                                    codeWriter.WriteBlankLine();
                                    codeWriter.WriteLine(encounteredInitializeUninstall ? "Result := this_InitializeUninstall();" : "Result := True;");
                                }

                                codeWriter.WriteEnd();
                            }
                        }
                    }
                }
            }
        }