- tweak: remove dependency from Nebula.Shared

This commit is contained in:
2025-05-02 10:56:19 +03:00
parent 7187b4fba8
commit 4178c173e1
14 changed files with 461 additions and 76 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ obj/
riderModule.iml
/_ReSharper.Caches/
release/
publish/

View File

@@ -1,8 +1,6 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Shared;
namespace Nebula.UpdateResolver;
@@ -15,15 +13,11 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
var services = new ServiceCollection();
services.AddServices();
services.AddTransient<MainWindow>();
var serviceProvider = services.BuildServiceProvider();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = serviceProvider.GetService<MainWindow>();
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();

View File

@@ -0,0 +1,16 @@
using System;
namespace Nebula.UpdateResolver.Configuration;
public class ConVar<T>
{
public ConVar(string name, T? defaultValue = default)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
DefaultValue = defaultValue;
}
public string Name { get; }
public Type Type => typeof(T);
public T? DefaultValue { get; }
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Nebula.UpdateResolver.Configuration;
public static class ConVarBuilder
{
public static ConVar<T> Build<T>(string name, T? defaultValue = default)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("ConVar name cannot be null or whitespace.", nameof(name));
return new ConVar<T>(name, defaultValue);
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Json;
namespace Nebula.UpdateResolver.Configuration;
public static class ConfigurationStandalone
{
private static FileApi _fileApi = new FileApi(Path.Join(MainWindow.RootPath, "config"));
public static T? GetConfigValue<T>(ConVar<T> conVar)
{
ArgumentNullException.ThrowIfNull(conVar);
try
{
if (_fileApi.TryOpen(GetFileName(conVar), out var stream))
using (stream)
{
var obj = JsonSerializer.Deserialize<T>(stream);
if (obj != null)
{
Console.WriteLine($"Successfully loaded config: {conVar.Name}");
return obj;
}
}
}
catch (Exception e)
{
Console.WriteLine($"Error loading config for {conVar.Name}: {e.Message}");
}
Console.WriteLine($"Using default value for config: {conVar.Name}");
return conVar.DefaultValue;
}
public static bool TryGetConfigValue<T>(ConVar<T> conVar,
[NotNullWhen(true)] out T? value)
{
ArgumentNullException.ThrowIfNull(conVar);
value = default;
try
{
if (_fileApi.TryOpen(GetFileName(conVar), out var stream))
using (stream)
{
var obj = JsonSerializer.Deserialize<T>(stream);
if (obj != null)
{
Console.WriteLine($"Successfully loaded config: {conVar.Name}");
value = obj;
return true;
}
}
}
catch (Exception e)
{
Console.WriteLine($"Error loading config for {conVar.Name}: {e.Message}");
}
Console.WriteLine($"Using default value for config: {conVar.Name}");
return false;
}
public static void SetConfigValue<T>(ConVar<T> conVar, T value)
{
ArgumentNullException.ThrowIfNull(conVar);
if (value == null) throw new ArgumentNullException(nameof(value));
if (!conVar.Type.IsInstanceOfType(value))
{
return;
}
try
{
var serializedData = JsonSerializer.Serialize(value);
using var stream = new MemoryStream();
using var writer = new StreamWriter(stream);
writer.Write(serializedData);
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
_fileApi.Save(GetFileName(conVar), stream);
}
catch (Exception e)
{
}
}
private static string GetFileName<T>(ConVar<T> conVar)
{
return $"{conVar.Name}.json";
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
namespace Nebula.UpdateResolver;
public sealed class FileApi
{
public string RootPath;
public FileApi(string rootPath)
{
RootPath = rootPath;
}
public bool TryOpen(string path,[NotNullWhen(true)] out Stream? stream)
{
var fullPath = Path.Join(RootPath, path);
if (File.Exists(fullPath))
try
{
stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return true;
}
catch
{
stream = null;
return false;
}
stream = null;
return false;
}
public bool Save(string path, Stream input)
{
var currPath = Path.Join(RootPath, path);
try
{
var dirInfo = new DirectoryInfo(Path.GetDirectoryName(currPath) ?? throw new InvalidOperationException());
if (!dirInfo.Exists) dirInfo.Create();
using var stream = new FileStream(currPath, FileMode.Create, FileAccess.Write, FileShare.None);
input.CopyTo(stream);
return true;
}
catch
{
return false;
}
}
public bool Remove(string path)
{
var fullPath = Path.Join(RootPath, path);
try
{
if (File.Exists(fullPath))
{
File.Delete(fullPath);
return true;
}
}
catch
{
// Log exception if necessary
}
return false;
}
public bool Has(string path)
{
var fullPath = Path.Join(RootPath, path);
return File.Exists(fullPath);
}
private IEnumerable<string> GetAllFiles(){
if(!Directory.Exists(RootPath)) return [];
return Directory.EnumerateFiles(RootPath, "*.*", SearchOption.AllDirectories).Select(p=>p.Replace(RootPath,"").Substring(1));
}
public IEnumerable<string> AllFiles => GetAllFiles();
}

View File

@@ -7,32 +7,28 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Nebula.Shared.FileApis;
using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Tmds.DBus.Protocol;
using Nebula.UpdateResolver.Configuration;
using Nebula.UpdateResolver.Rest;
namespace Nebula.UpdateResolver;
public partial class MainWindow : Window
{
private readonly ConfigurationService _configurationService;
private readonly RestService _restService;
public static readonly string RootPath = Path.Join(Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData), "Datum");
private readonly HttpClient _httpClient = new HttpClient();
public FileApi FileApi { get; set; }
public readonly FileApi FileApi = new FileApi(Path.Join(RootPath,"app"));
public MainWindow(FileService fileService, ConfigurationService configurationService, RestService restService)
public MainWindow()
{
_configurationService = configurationService;
_restService = restService;
InitializeComponent();
FileApi = (FileApi)fileService.CreateFileApi("app");
Start();
}
private async Task Start()
{
try
{
var info = await EnsureFiles();
Log("Downloading files...");
@@ -52,7 +48,7 @@ public partial class MainWindow : Window
foreach (var file in info.ToDownload)
{
using var response = await _httpClient.GetAsync(
_configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl)
ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)
+ "/" + file.Hash);
response.EnsureSuccessStatusCode();
@@ -64,6 +60,7 @@ public partial class MainWindow : Window
loadedManifest.Add(file);
Save(loadedManifest);
}
Log("Download finished. Running launcher...");
var process = Process.Start(new ProcessStartInfo
@@ -76,6 +73,11 @@ public partial class MainWindow : Window
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8
});
}
catch (Exception e)
{
Log("Error! " + e.Message);
}
Thread.Sleep(2000);
@@ -85,29 +87,29 @@ public partial class MainWindow : Window
private async Task<ManifestEnsureInfo> EnsureFiles()
{
Log("Ensuring launcher manifest...");
var manifest = await _restService.GetAsync<LauncherManifest>(
new Uri(_configurationService.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
var manifest = await RestStandalone.GetAsync<LauncherManifest>(
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
var toDownload = new HashSet<LauncherManifestEntry>();
var toDelete = new HashSet<LauncherManifestEntry>();
var filesExist = new HashSet<LauncherManifestEntry>();
Log("Manifest loaded!");
if (_configurationService.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest))
if (ConfigurationStandalone.TryGetConfigValue(UpdateConVars.CurrentLauncherManifest, out var currentManifest))
{
Log("Delta manifest loaded!");
foreach (var file in currentManifest.Entries)
{
if (!manifest.Entries.Contains(file))
toDelete.Add(file);
toDelete.Add(EnsurePath(file));
else
filesExist.Add(file);
filesExist.Add(EnsurePath(file));
}
foreach (var file in manifest.Entries)
{
if(!currentManifest.Entries.Contains(file))
toDownload.Add(file);
toDownload.Add(EnsurePath(file));
}
}
else
@@ -133,8 +135,43 @@ public partial class MainWindow : Window
private void Save(HashSet<LauncherManifestEntry> entries)
{
_configurationService.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries));
ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries));
}
private LauncherManifestEntry EnsurePath(LauncherManifestEntry entry)
{
if(!PathValidator.IsSafePath(FileApi.RootPath, entry.Path))
throw new ArgumentException("Path contains invalid characters. Manifest hash: " + entry.Hash);
return entry;
}
}
public record struct ManifestEnsureInfo(HashSet<LauncherManifestEntry> ToDownload, HashSet<LauncherManifestEntry> ToDelete, HashSet<LauncherManifestEntry> FilesExist);
public static class PathValidator
{
public static bool IsSafePath(string baseDirectory, string relativePath)
{
if (Path.IsPathRooted(relativePath))
return false;
var fullBase = Path.GetFullPath(baseDirectory);
var combinedPath = Path.Combine(fullBase, relativePath);
var fullPath = Path.GetFullPath(combinedPath);
if (!fullPath.StartsWith(fullBase, StringComparison.Ordinal))
return false;
if (File.Exists(fullPath) || Directory.Exists(fullPath))
{
FileInfo fileInfo = new FileInfo(fullPath);
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
return false;
}
return true;
}
}

View File

@@ -0,0 +1,5 @@
using System.Collections.Generic;
namespace Nebula.UpdateResolver;
public record struct ManifestEnsureInfo(HashSet<LauncherManifestEntry> ToDownload, HashSet<LauncherManifestEntry> ToDelete, HashSet<LauncherManifestEntry> FilesExist);

View File

@@ -6,6 +6,9 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<PublishSingleFile>true</PublishSingleFile>
<!--<PublishTrimmed>true</PublishTrimmed>-->
<SelfContained>true</SelfContained>
</PropertyGroup>
<ItemGroup>
@@ -22,10 +25,5 @@
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,39 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Nebula.UpdateResolver.Rest;
public static class Helper
{
public static readonly JsonSerializerOptions JsonWebOptions = new(JsonSerializerDefaults.Web);
public static void SafeOpenBrowser(string uri)
{
if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
{
Console.WriteLine("Unable to parse URI in server-provided link: {Link}", uri);
return;
}
if (parsedUri.Scheme is not ("http" or "https"))
{
Console.WriteLine("Refusing to open server-provided link {Link}, only http/https are allowed", parsedUri);
return;
}
OpenBrowser(parsedUri.ToString());
}
public static void OpenBrowser(string url)
{
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
public static async Task<T> AsJson<T>(this HttpContent content) where T : notnull
{
var str = await content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(str, JsonWebOptions) ??
throw new JsonException("AsJson: did not expect null response");
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Net;
using System.Net.Http;
namespace Nebula.UpdateResolver.Rest;
public sealed class RestRequestException(HttpContent content, HttpStatusCode statusCode) : Exception
{
public HttpStatusCode StatusCode { get; } = statusCode;
public HttpContent Content { get; } = content;
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Nebula.UpdateResolver.Rest;
public static class RestStandalone
{
private static readonly HttpClient _client = new();
private static readonly JsonSerializerOptions _serializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
public static async Task<T> GetAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
{
var response = await _client.GetAsync(uri, cancellationToken);
return await ReadResult<T>(response, cancellationToken);
}
public static async Task<T> GetAsyncDefault<T>(Uri uri, T defaultValue, CancellationToken cancellationToken) where T : notnull
{
try
{
return await GetAsync<T>(uri, cancellationToken);
}
catch (Exception e)
{
return defaultValue;
}
}
public static async Task<K> PostAsync<K, T>(T information, Uri uri, CancellationToken cancellationToken) where K : notnull
{
var json = JsonSerializer.Serialize(information, _serializerOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync(uri, content, cancellationToken);
return await ReadResult<K>(response, cancellationToken);
}
public static async Task<T> PostAsync<T>(Stream stream, Uri uri, CancellationToken cancellationToken) where T : notnull
{
using var multipartFormContent =
new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
multipartFormContent.Add(new StreamContent(stream), "formFile", "image.png");
var response = await _client.PostAsync(uri, multipartFormContent, cancellationToken);
return await ReadResult<T>(response, cancellationToken);
}
public static async Task<T> DeleteAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
{
var response = await _client.DeleteAsync(uri, cancellationToken);
return await ReadResult<T>(response, cancellationToken);
}
private static async Task<T> ReadResult<T>(HttpResponseMessage response, CancellationToken cancellationToken) where T : notnull
{
var content = await response.Content.ReadAsStringAsync(cancellationToken);
if (typeof(T) == typeof(string) && content is T t)
return t;
if (response.IsSuccessStatusCode)
{
return await response.Content.AsJson<T>();
}
throw new RestRequestException(response.Content, response.StatusCode);
}
}

View File

@@ -1,6 +1,4 @@
using System;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.UpdateResolver.Configuration;
namespace Nebula.UpdateResolver;

View File

@@ -1,4 +1,13 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACancellationToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2565b9d99fdde488bc7801b84387b2cc864959cfb63212e1ff576fc9c6bb7e_003FCancellationToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConsole_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ffd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1_003FConsole_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFrozenDictionary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F89dff9063ddb01ff8125b579122b88bf4de94526490d77bcbbef7d0ee662a_003FFrozenDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9657cc383c70851dc2bdcf91eff27f21196844abfe552fc9c3243ff36974cd_003FHttpContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializer_002ERead_002EString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27c4858128168eda568c1334d70d5241efb9461e2a3209258a04deee5d9c367_003FJsonSerializer_002ERead_002EString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa8ceca48b7b645dd875a40ee6d28725416d08_003F1b_003F6cd78dc8_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionContainerBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa8ceca48b7b645dd875a40ee6d28725416d08_003F1b_003F6cd78dc8_003FServiceCollectionContainerBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002EManipulation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe75a5575ba872c8ea754c015cb363850e6c661f39569712d5b74aaca67263c_003FString_002EManipulation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>