├── asset ├── Piggy-400px.png ├── Piggy-Icon-128px.ico ├── Piggy-Icon-128px.png ├── Piggy.svg └── PiggyLicense.rtf ├── src └── Datalust.Piggy │ ├── Attribution │ ├── Dapper License.txt │ ├── Npgsql License.txt │ ├── Autofac License.txt │ └── Serilog License.txt │ ├── Properties │ └── AssemblyInfo.cs │ ├── Syntax │ ├── ChangeScriptDirectives.cs │ ├── ChangeScriptDirectiveParser.cs │ └── ChangeScriptVariableProcessor.cs │ ├── Cli │ ├── ICommandMetadata.cs │ ├── CommandMetadata.cs │ ├── CommandFeature.cs │ ├── Requirement.cs │ ├── CommandAttribute.cs │ ├── Commands │ │ ├── VersionCommand.cs │ │ ├── BaselineCommand.cs │ │ ├── LogCommand.cs │ │ ├── PendingCommand.cs │ │ ├── UpdateCommand.cs │ │ └── HelpCommand.cs │ ├── Features │ │ ├── ScriptRootFeature.cs │ │ ├── DefineVariablesFeature.cs │ │ ├── UsernamePasswordFeature.cs │ │ ├── DatabaseFeature.cs │ │ └── LoggingFeature.cs │ ├── Printing.cs │ ├── CommandLineHost.cs │ ├── Command.cs │ └── Options.cs │ ├── Program.cs │ ├── Filesystem │ ├── ChangeScriptFile.cs │ └── ChangeScriptFileEnumerator.cs │ ├── History │ ├── AppliedChangeScript.cs │ └── AppliedChangeScriptLog.cs │ ├── PiggyModule.cs │ ├── Status │ └── DatabaseStatus.cs │ ├── Database │ ├── ConnectionStringParser.cs │ └── DatabaseConnector.cs │ ├── Baseline │ └── BaselineSession.cs │ ├── Datalust.Piggy.csproj │ └── Update │ └── UpdateSession.cs ├── global.json ├── test └── Datalust.Piggy.Tests │ ├── Cli │ └── Commands │ │ └── VersionCommandTests.cs │ ├── PiggyModuleTests.cs │ ├── Datalust.Piggy.Tests.csproj │ ├── Syntax │ ├── ChangeScriptVariableProcessorTests.cs │ └── ChangeScriptDirectiveParserTests.cs │ └── Database │ └── ConnectionStringParserTests.cs ├── piggy.sln.DotSettings ├── appveyor.yml ├── piggy.sln ├── README.md ├── Build.ps1 ├── .gitignore └── LICENSE /asset/Piggy-400px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datalust/piggy/HEAD/asset/Piggy-400px.png -------------------------------------------------------------------------------- /src/Datalust.Piggy/Attribution/Dapper License.txt: -------------------------------------------------------------------------------- 1 | http://www.apache.org/licenses/LICENSE-2.0 2 | -------------------------------------------------------------------------------- /asset/Piggy-Icon-128px.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datalust/piggy/HEAD/asset/Piggy-Icon-128px.ico -------------------------------------------------------------------------------- /asset/Piggy-Icon-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datalust/piggy/HEAD/asset/Piggy-Icon-128px.png -------------------------------------------------------------------------------- /src/Datalust.Piggy/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Datalust.Piggy.Tests")] 4 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "allowPrerelease": false, 4 | "version": "8.0.204", 5 | "rollForward": "latestFeature" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Syntax/ChangeScriptDirectives.cs: -------------------------------------------------------------------------------- 1 | namespace Datalust.Piggy.Syntax 2 | { 3 | class ChangeScriptDirectives 4 | { 5 | public bool IsTransacted { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/ICommandMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Datalust.Piggy.Cli 2 | { 3 | interface ICommandMetadata 4 | { 5 | string? Name { get; } 6 | string? HelpText { get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/CommandMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Datalust.Piggy.Cli 2 | { 3 | public class CommandMetadata : ICommandMetadata 4 | { 5 | public string? Name { get; set; } 6 | public string? HelpText { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/CommandFeature.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Datalust.Piggy.Cli 5 | { 6 | abstract class CommandFeature 7 | { 8 | public abstract void Enable(OptionSet options); 9 | 10 | public virtual IEnumerable GetUsageErrors() => Array.Empty(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Program.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Datalust.Piggy; 3 | using Datalust.Piggy.Cli; 4 | using Serilog; 5 | 6 | var builder = new ContainerBuilder(); 7 | builder.RegisterModule(); 8 | 9 | await using var container = builder.Build(); 10 | var commandLineHost = container.Resolve(); 11 | var exit = commandLineHost.Run(args); 12 | await Log.CloseAndFlushAsync(); 13 | return exit; 14 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Requirement.cs: -------------------------------------------------------------------------------- 1 | namespace Datalust.Piggy.Cli 2 | { 3 | static class Requirement 4 | { 5 | public static bool IsNonEmpty(string? value) 6 | { 7 | return !string.IsNullOrWhiteSpace(value); 8 | } 9 | 10 | public static string NonEmptyDescription(string optionName) 11 | { 12 | return $"a {optionName} is required"; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Filesystem/ChangeScriptFile.cs: -------------------------------------------------------------------------------- 1 | namespace Datalust.Piggy.Filesystem 2 | { 3 | class ChangeScriptFile 4 | { 5 | public string RelativeName { get; set; } 6 | public string FullPath { get; set; } 7 | 8 | public ChangeScriptFile(string fullPath, string relativeName) 9 | { 10 | FullPath = fullPath; 11 | RelativeName = relativeName; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/History/AppliedChangeScript.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Datalust.Piggy.History 4 | { 5 | // ReSharper disable once ClassNeverInstantiated.Global 6 | class AppliedChangeScript 7 | { 8 | public string? ScriptFile { get; set; } 9 | public DateTimeOffset AppliedAt { get; set; } 10 | public string? AppliedBy { get; set; } 11 | public int AppliedOrder { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/CommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Datalust.Piggy.Cli 4 | { 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public class CommandAttribute : Attribute, ICommandMetadata 7 | { 8 | public string Name { get; private set; } 9 | public string HelpText { get; private set; } 10 | 11 | public CommandAttribute(string name, string helpText) 12 | { 13 | Name = name; 14 | HelpText = helpText; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/Datalust.Piggy.Tests/Cli/Commands/VersionCommandTests.cs: -------------------------------------------------------------------------------- 1 | using Datalust.Piggy.Cli.Commands; 2 | using Xunit; 3 | 4 | namespace Datalust.Piggy.Tests.Cli.Commands 5 | { 6 | public class VersionCommandTests 7 | { 8 | #if !DEBUG 9 | [Fact] 10 | public void VersionIsSet() 11 | { 12 | var version = VersionCommand.GetVersion(); 13 | Assert.False(string.IsNullOrEmpty(version)); 14 | Assert.NotEqual("1.0.0", version); 15 | Assert.NotEqual("0.0.0", version); 16 | } 17 | #endif 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Syntax/ChangeScriptDirectiveParser.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Datalust.Piggy.Syntax 4 | { 5 | static class ChangeScriptDirectiveParser 6 | { 7 | public static ChangeScriptDirectives Parse(string scriptContent) 8 | { 9 | var firstLine = new StringReader(scriptContent).ReadLine(); 10 | var result = new ChangeScriptDirectives 11 | { 12 | IsTransacted = !(firstLine?.Trim().Equals("-- PIGGY NO TRANSACTION") ?? false) 13 | }; 14 | return result; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Syntax/ChangeScriptVariableProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Datalust.Piggy.Syntax 4 | { 5 | static class ChangeScriptVariableProcessor 6 | { 7 | public static string InsertVariables(string content, IReadOnlyDictionary variables) 8 | { 9 | var result = content; 10 | 11 | foreach (var variable in variables) 12 | { 13 | var token = $"${variable.Key}$"; 14 | result = result.Replace(token, variable.Value); 15 | } 16 | 17 | return result; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Datalust.Piggy.Tests/PiggyModuleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Datalust.Piggy.Cli; 3 | using Xunit; 4 | 5 | namespace Datalust.Piggy.Tests; 6 | 7 | public class PiggyModuleTests 8 | { 9 | [Fact] 10 | public void AllCommandsScannedAndFoundAreExplicitlyRegisteredInPiggyModule() 11 | { 12 | var expectedCommands = typeof(PiggyModule).Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Command))); 13 | var actualCommands = PiggyModule.RegisteredCommands; 14 | 15 | foreach (var expected in expectedCommands) 16 | { 17 | Assert.Contains(expected, actualCommands); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /piggy.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | True 4 | True 5 | True 6 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | skip_tags: true 3 | image: Visual Studio 2022 4 | build_script: 5 | - ps: ./Build.ps1 6 | test: off 7 | artifacts: 8 | - path: .artifacts/piggy-*.tar.gz 9 | - path: .artifacts/piggy-*.zip 10 | - path: .artifacts/Datalust.Piggy.*.nupkg 11 | deploy: 12 | - provider: NuGet 13 | api_key: 14 | secure: 0QScMsq2IR0gpicVnNS4mQlrcLVrjt/YXUYGDZzcx110LJ4fy1cEEyd5YMrhoH7U 15 | skip_symbols: true 16 | on: 17 | branch: /^(main|dev)$/ 18 | - provider: GitHub 19 | auth_token: 20 | secure: Bo3ypKpKFxinjR9ShkNekNvkob2iklHJU+UlYyfHtcFFIAa58SV2TkEd0xWxz633 21 | artifact: /piggy-.*\.(zip|tar\.gz)/ 22 | tag: v$(appveyor_build_version) 23 | on: 24 | branch: main 25 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Commands/VersionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Datalust.Piggy.Cli.Commands 5 | { 6 | [Command("--version", "Print the current executable version")] 7 | class VersionCommand : Command 8 | { 9 | protected override int Run() 10 | { 11 | var version = GetVersion(); 12 | Console.WriteLine(version); 13 | return 0; 14 | } 15 | 16 | public static string GetVersion() 17 | { 18 | return typeof(VersionCommand).GetTypeInfo().Assembly 19 | .GetCustomAttribute()!.InformationalVersion; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Features/ScriptRootFeature.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Datalust.Piggy.Cli.Features 4 | { 5 | class ScriptRootFeature : CommandFeature 6 | { 7 | public string? ScriptRoot { get; set; } 8 | 9 | public override void Enable(OptionSet options) 10 | { 11 | options.Add( 12 | "s=|script-root=", 13 | "The root directory to search for scripts", 14 | v => ScriptRoot = v); 15 | } 16 | 17 | public override IEnumerable GetUsageErrors() 18 | { 19 | if (!Requirement.IsNonEmpty(ScriptRoot)) yield return Requirement.NonEmptyDescription("script root directory"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/Datalust.Piggy.Tests/Datalust.Piggy.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/Datalust.Piggy.Tests/Syntax/ChangeScriptVariableProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Datalust.Piggy.Syntax; 3 | using Xunit; 4 | 5 | namespace Datalust.Piggy.Tests.Syntax 6 | { 7 | public class ChangeScriptVariableProcessorTests 8 | { 9 | [Fact] 10 | public void ConfiguredVariablesAreReplaced() 11 | { 12 | var variables = new Dictionary 13 | { 14 | ["one"] = "1", 15 | ["two"] = "2" 16 | }; 17 | 18 | const string content = "$$one$-$two$-$two$-$three$"; 19 | var result = ChangeScriptVariableProcessor.InsertVariables(content, variables); 20 | Assert.Equal("$1-2-2-$three$", result); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/Datalust.Piggy.Tests/Syntax/ChangeScriptDirectiveParserTests.cs: -------------------------------------------------------------------------------- 1 | using Datalust.Piggy.Syntax; 2 | using Xunit; 3 | 4 | namespace Datalust.Piggy.Tests.Syntax 5 | { 6 | public class ChangeScriptDirectiveParserTests 7 | { 8 | [Fact] 9 | public void ByDefaultScriptsAreTransacted() 10 | { 11 | var directives = ChangeScriptDirectiveParser.Parse("INSERT INTO foo (id) VALUES (1)"); 12 | Assert.True(directives.IsTransacted); 13 | } 14 | 15 | [Fact] 16 | public void DirectiveCanSpecifyNoTransaction() 17 | { 18 | var directives = ChangeScriptDirectiveParser.Parse("-- PIGGY NO TRANSACTION\nINSERT INTO foo (id) VALUES (1)"); 19 | Assert.False(directives.IsTransacted); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/PiggyModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Autofac; 3 | using Datalust.Piggy.Cli; 4 | using Datalust.Piggy.Cli.Commands; 5 | 6 | namespace Datalust.Piggy 7 | { 8 | class PiggyModule : Module 9 | { 10 | public static readonly Type[] RegisteredCommands = 11 | { 12 | typeof(BaselineCommand), 13 | typeof(HelpCommand), 14 | typeof(LogCommand), 15 | typeof(PendingCommand), 16 | typeof(UpdateCommand), 17 | typeof(VersionCommand) 18 | }; 19 | 20 | protected override void Load(ContainerBuilder builder) 21 | { 22 | builder.RegisterType(); 23 | builder.RegisterTypes(RegisteredCommands).As().WithMetadataFrom(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Features/DefineVariablesFeature.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Datalust.Piggy.Cli.Features 4 | { 5 | class DefineVariablesFeature : CommandFeature 6 | { 7 | readonly Dictionary _variables = new Dictionary(); 8 | 9 | public Dictionary Variables => _variables; 10 | 11 | public override void Enable(OptionSet options) 12 | { 13 | options.Add( 14 | "v={=}|variable={=}", 15 | "Define variables to be $substituted$ into scripts, e.g. -v Customer=C123 -v Environment=Production", 16 | (n, v) => 17 | { 18 | var name = n.Trim(); 19 | var valueText = v?.Trim() ?? ""; 20 | _variables.Add(name, valueText); 21 | }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Features/UsernamePasswordFeature.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Datalust.Piggy.Cli.Features 4 | { 5 | class UsernamePasswordFeature : CommandFeature 6 | { 7 | public string? Username { get; private set; } 8 | 9 | public string? Password { get; private set; } 10 | 11 | public override void Enable(OptionSet options) 12 | { 13 | options.Add("u=|username=", "The name of the user account", v => Username = v); 14 | options.Add("p=|password=", "The password for the user account", v => Password = v); 15 | } 16 | 17 | public override IEnumerable GetUsageErrors() 18 | { 19 | if (!Requirement.IsNonEmpty(Username)) yield return Requirement.NonEmptyDescription("username"); 20 | if (!Requirement.IsNonEmpty(Password)) yield return Requirement.NonEmptyDescription("password"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Printing.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | 4 | namespace Datalust.Piggy.Cli 5 | { 6 | static class Printing 7 | { 8 | const int ConsoleWidth = 80; 9 | 10 | public static void Define(string term, string definition, int termColumnWidth, TextWriter output) 11 | { 12 | var header = term.PadRight(termColumnWidth); 13 | var right = ConsoleWidth - header.Length; 14 | 15 | var rest = definition.ToCharArray(); 16 | while (rest.Any()) 17 | { 18 | var content = new string(rest.Take(right).ToArray()); 19 | if (!string.IsNullOrWhiteSpace(content)) 20 | { 21 | output.Write(header); 22 | header = new string(' ', header.Length); 23 | output.WriteLine(content); 24 | } 25 | rest = rest.Skip(right).ToArray(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Status/DatabaseStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Datalust.Piggy.Filesystem; 5 | using Datalust.Piggy.History; 6 | using Npgsql; 7 | 8 | namespace Datalust.Piggy.Status 9 | { 10 | static class DatabaseStatus 11 | { 12 | public static ChangeScriptFile[] GetPendingScripts(NpgsqlConnection connection, string scriptRoot) 13 | { 14 | if (connection == null) throw new ArgumentNullException(nameof(connection)); 15 | if (scriptRoot == null) throw new ArgumentNullException(nameof(scriptRoot)); 16 | 17 | var changes = AppliedChangeScriptLog.GetAppliedChangeScripts(connection); 18 | 19 | var applied = new HashSet(changes.Select(m => m.ScriptFile!)); 20 | return ChangeScriptFileEnumerator.EnumerateInOrder(scriptRoot) 21 | .Where(s => !applied.Contains(s.RelativeName)) 22 | .ToArray(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Features/DatabaseFeature.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Datalust.Piggy.Cli.Features 4 | { 5 | class DatabaseFeature : CommandFeature 6 | { 7 | public string? Host { get; set; } 8 | public string? Database { get; set; } 9 | 10 | public override void Enable(OptionSet options) 11 | { 12 | options.Add( 13 | "h=|host=", 14 | "The PostgreSQL host", 15 | v => Host = v); 16 | 17 | options.Add( 18 | "d=|database=", 19 | "The database to apply changes to", 20 | v => Database = v); 21 | } 22 | 23 | public override IEnumerable GetUsageErrors() 24 | { 25 | if (!Requirement.IsNonEmpty(Host)) yield return Requirement.NonEmptyDescription("host"); 26 | if (!Requirement.IsNonEmpty(Database)) yield return Requirement.NonEmptyDescription("database"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Attribution/Npgsql License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2002-2017, The Npgsql Development Team 2 | 3 | Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. 4 | 5 | IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 6 | 7 | THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 8 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Database/ConnectionStringParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Datalust.Piggy.Database 6 | { 7 | public static class ConnectionStringParser 8 | { 9 | const StringSplitOptions Options = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; 10 | 11 | // e.g. Host=localhost;Username=postgres;Password=password-value;Database=database-name 12 | public static Dictionary Parse(string? connectionString) 13 | { 14 | var parts = new Dictionary(); 15 | 16 | if (connectionString == null) return parts; 17 | 18 | connectionString.Split(';', Options) 19 | .Where(s => s.Contains('=')) 20 | .Select(s => s.Split('=', 2, Options)) 21 | .Where(a => a.Length == 2) 22 | .ToList() 23 | .ForEach(a => parts.TryAdd(a[0].Trim(), a[1].Trim())); 24 | 25 | return parts; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Attribution/Autofac License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Autofac Project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Filesystem/ChangeScriptFileEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Serilog; 5 | 6 | namespace Datalust.Piggy.Filesystem 7 | { 8 | static class ChangeScriptFileEnumerator 9 | { 10 | public static ChangeScriptFile[] EnumerateInOrder(string scriptRoot) 11 | { 12 | Log.Information("Searching for *.sql change script files in {ScriptRoot}", scriptRoot); 13 | 14 | if (!Directory.Exists(scriptRoot)) 15 | throw new ArgumentException("The script root directory does not exist"); 16 | 17 | var root = Path.GetFullPath(scriptRoot); 18 | if (!root.EndsWith(Path.DirectorySeparatorChar.ToString())) 19 | root += Path.DirectorySeparatorChar; 20 | var prefixLength = root.Length; 21 | 22 | return Directory.GetFiles(scriptRoot, "*.sql", SearchOption.AllDirectories) 23 | .Select(Path.GetFullPath) 24 | .Select(fp => new ChangeScriptFile(fp, fp.Substring(prefixLength).Replace(Path.DirectorySeparatorChar, '/'))) 25 | .OrderBy(csc => csc.RelativeName) 26 | .ToArray(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/CommandLineHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Autofac.Features.Metadata; 6 | 7 | namespace Datalust.Piggy.Cli 8 | { 9 | class CommandLineHost 10 | { 11 | readonly List, CommandMetadata>> _availableCommands; 12 | 13 | public CommandLineHost(IEnumerable, CommandMetadata>> availableCommands) 14 | { 15 | _availableCommands = availableCommands.ToList(); 16 | } 17 | 18 | public int Run(string[] args) 19 | { 20 | var ea = Assembly.GetEntryAssembly()!; 21 | var name = ea.GetName().Name; 22 | 23 | if (args.Length > 0) 24 | { 25 | var norm = args[0].ToLowerInvariant(); 26 | var cmd = _availableCommands.SingleOrDefault(c => c.Metadata.Name == norm); 27 | if (cmd != null) 28 | { 29 | return cmd.Value.Value.Invoke(args.Skip(1).ToArray()); 30 | } 31 | } 32 | 33 | Console.WriteLine($"Usage: {name} []"); 34 | Console.WriteLine($"Type `{name} help` for available commands"); 35 | return -1; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Features/LoggingFeature.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Serilog; 3 | using Serilog.Events; 4 | 5 | namespace Datalust.Piggy.Cli.Features 6 | { 7 | class LoggingFeature : CommandFeature 8 | { 9 | string? _serverUrl, _apiKey; 10 | LogEventLevel _level = LogEventLevel.Information, _consoleLevel = LevelAlias.Minimum; 11 | 12 | public override void Enable(OptionSet options) 13 | { 14 | options.Add("quiet", "Limit diagnostic terminal output to errors only", v => _consoleLevel = LogEventLevel.Error); 15 | options.Add("debug", "Write additional diagnostic log output", v => _level = LogEventLevel.Debug); 16 | options.Add("log-seq=", "Log output to a Seq server at the specified URL", v => _serverUrl = v); 17 | options.Add("log-seq-apikey=", "If logging to Seq, an optional API key", v => _apiKey = v); 18 | } 19 | 20 | public void Configure() 21 | { 22 | var loggerConfiguration = new LoggerConfiguration() 23 | .MinimumLevel.Is(_level) 24 | .Enrich.WithProperty("Application", "Piggy") 25 | .Enrich.WithProperty("Invocation", Guid.NewGuid()) 26 | .WriteTo.Console(standardErrorFromLevel: LogEventLevel.Error, restrictedToMinimumLevel: _consoleLevel); 27 | 28 | if (!string.IsNullOrWhiteSpace(_serverUrl)) 29 | loggerConfiguration 30 | .WriteTo.Seq(_serverUrl, apiKey: _apiKey); 31 | 32 | Log.Logger = loggerConfiguration.CreateLogger(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Commands/BaselineCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Datalust.Piggy.Baseline; 3 | using Datalust.Piggy.Cli.Features; 4 | using Datalust.Piggy.Update; 5 | using Npgsql; 6 | using Serilog; 7 | 8 | namespace Datalust.Piggy.Cli.Commands 9 | { 10 | [Command("baseline", "Add scripts to the change log without running them")] 11 | class BaselineCommand : Command 12 | { 13 | readonly UsernamePasswordFeature _usernamePasswordFeature; 14 | readonly DatabaseFeature _databaseFeature; 15 | readonly LoggingFeature _loggingFeature; 16 | readonly ScriptRootFeature _scriptRootFeature; 17 | 18 | public BaselineCommand() 19 | { 20 | _databaseFeature = Enable(); 21 | _usernamePasswordFeature = Enable(); 22 | _scriptRootFeature = Enable(); 23 | _loggingFeature = Enable(); 24 | } 25 | 26 | protected override int Run() 27 | { 28 | _loggingFeature.Configure(); 29 | 30 | try 31 | { 32 | BaselineSession.BaselineDatabase( 33 | _databaseFeature.Host!, _databaseFeature.Database!, _usernamePasswordFeature.Username!, _usernamePasswordFeature.Password!, 34 | _scriptRootFeature.ScriptRoot!); 35 | 36 | return 0; 37 | } 38 | catch (Exception ex) 39 | { 40 | Log.Fatal(ex, "Could not baseline the database"); 41 | return -1; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Commands/LogCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Datalust.Piggy.Cli.Features; 3 | using Datalust.Piggy.Database; 4 | using Datalust.Piggy.History; 5 | using Serilog; 6 | 7 | namespace Datalust.Piggy.Cli.Commands 8 | { 9 | [Command("log", "List change scripts that have been applied to a database")] 10 | class LogCommand : Command 11 | { 12 | readonly UsernamePasswordFeature _usernamePasswordFeature; 13 | readonly DatabaseFeature _databaseFeature; 14 | readonly LoggingFeature _loggingFeature; 15 | 16 | public LogCommand() 17 | { 18 | _databaseFeature = Enable(); 19 | _usernamePasswordFeature = Enable(); 20 | _loggingFeature = Enable(); 21 | } 22 | 23 | protected override int Run() 24 | { 25 | _loggingFeature.Configure(); 26 | 27 | try 28 | { 29 | using var connection = DatabaseConnector.Connect(_databaseFeature.Host!, _databaseFeature.Database!, 30 | _usernamePasswordFeature.Username!, _usernamePasswordFeature.Password!, false); 31 | foreach (var applied in AppliedChangeScriptLog.GetAppliedChangeScripts(connection)) 32 | { 33 | Console.WriteLine($"{applied.AppliedAt:o} {applied.AppliedBy} {applied.ScriptFile}"); 34 | } 35 | 36 | return 0; 37 | } 38 | catch (Exception ex) 39 | { 40 | Log.Fatal(ex, "Could not list change scripts"); 41 | return -1; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Commands/PendingCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Datalust.Piggy.Cli.Features; 3 | using Datalust.Piggy.Database; 4 | using Datalust.Piggy.Status; 5 | using Serilog; 6 | 7 | namespace Datalust.Piggy.Cli.Commands 8 | { 9 | [Command("pending", "Determine which scripts will be run in an update")] 10 | class PendingCommand : Command 11 | { 12 | readonly UsernamePasswordFeature _usernamePasswordFeature; 13 | readonly DatabaseFeature _databaseFeature; 14 | readonly LoggingFeature _loggingFeature; 15 | readonly ScriptRootFeature _scriptRootFeature; 16 | 17 | public PendingCommand() 18 | { 19 | _databaseFeature = Enable(); 20 | _usernamePasswordFeature = Enable(); 21 | _scriptRootFeature = Enable(); 22 | _loggingFeature = Enable(); 23 | } 24 | 25 | protected override int Run() 26 | { 27 | _loggingFeature.Configure(); 28 | 29 | try 30 | { 31 | using (var connection = DatabaseConnector.Connect(_databaseFeature.Host!, _databaseFeature.Database!, 32 | _usernamePasswordFeature.Username!, _usernamePasswordFeature.Password!, false)) 33 | { 34 | foreach (var pending in DatabaseStatus.GetPendingScripts(connection, _scriptRootFeature.ScriptRoot!)) 35 | { 36 | Console.WriteLine($"{pending.RelativeName}"); 37 | } 38 | } 39 | 40 | return 0; 41 | } 42 | catch (Exception ex) 43 | { 44 | Log.Fatal(ex, "Could not determine pending change scripts"); 45 | return -1; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Baseline/BaselineSession.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Datalust.Piggy.Database; 3 | using Datalust.Piggy.History; 4 | using Datalust.Piggy.Status; 5 | using Npgsql; 6 | using Serilog; 7 | 8 | namespace Datalust.Piggy.Baseline 9 | { 10 | class BaselineSession 11 | { 12 | public static void BaselineDatabase(string host, string database, string username, string password, string scriptRoot) 13 | { 14 | using (var connection = DatabaseConnector.Connect(host, database, username, password, false)) 15 | { 16 | var scripts = DatabaseStatus.GetPendingScripts(connection, scriptRoot); 17 | 18 | Log.Information("Found {Count} script files without change log records", scripts.Length); 19 | 20 | if (scripts.Length != 0) 21 | { 22 | var commandText = new StringBuilder(); 23 | commandText.AppendLine("START TRANSACTION ISOLATION LEVEL REPEATABLE READ;"); 24 | commandText.AppendLine(AppliedChangeScriptLog.ChangesTableCreateScript); 25 | 26 | foreach (var script in scripts) 27 | { 28 | Log.Information("Recording {FullPath} as {ScriptFile}", script.FullPath, script.RelativeName); 29 | commandText.AppendLine(AppliedChangeScriptLog.CreateApplyLogScriptFor(script)); 30 | } 31 | 32 | commandText.AppendLine("COMMIT TRANSACTION;"); 33 | 34 | Log.Information("Writing change log"); 35 | using (var command = new NpgsqlCommand(commandText.ToString(), connection)) 36 | { 37 | command.ExecuteNonQuery(); 38 | } 39 | } 40 | 41 | Log.Information("Done"); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/History/AppliedChangeScriptLog.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Datalust.Piggy.Filesystem; 3 | using Npgsql; 4 | using Serilog; 5 | 6 | namespace Datalust.Piggy.History 7 | { 8 | static class AppliedChangeScriptLog 9 | { 10 | const string ChangesTableSchema = "piggy"; 11 | const string ChangesTableName = ChangesTableSchema + ".changes"; 12 | 13 | public const string ChangesTableCreateScript = 14 | @"CREATE SCHEMA IF NOT EXISTS " + ChangesTableSchema + @"; 15 | 16 | CREATE TABLE IF NOT EXISTS " + ChangesTableName + @" ( 17 | scriptfile varchar(1024) primary key, 18 | appliedat timestamptz not null default now(), 19 | appliedby varchar(1024) not null default current_user, 20 | appliedorder serial not null 21 | ); 22 | 23 | "; 24 | 25 | public static AppliedChangeScript[] GetAppliedChangeScripts(NpgsqlConnection connection) 26 | { 27 | Log.Information("Loading applied change scripts from the database"); 28 | 29 | try 30 | { 31 | var changes = connection.Query($"SELECT scriptfile, appliedat, appliedby, appliedorder FROM {ChangesTableName} ORDER BY appliedorder ASC;").AsList().ToArray(); 32 | Log.Information("Database history contains {AppliedCount} applied changes", changes.Length); 33 | return changes; 34 | } 35 | catch (PostgresException px) when (px.SqlState == "42P01") 36 | { 37 | Log.Information("Database history is empty (change log table does not exist)"); 38 | return new AppliedChangeScript[0]; 39 | } 40 | } 41 | 42 | public static string CreateApplyLogScriptFor(ChangeScriptFile script) 43 | { 44 | return "INSERT INTO " + ChangesTableName + $"(scriptfile) VALUES ('{script.RelativeName}');"; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Commands/UpdateCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Datalust.Piggy.Cli.Features; 3 | using Datalust.Piggy.Database; 4 | using Datalust.Piggy.Update; 5 | using Npgsql; 6 | using Serilog; 7 | 8 | namespace Datalust.Piggy.Cli.Commands 9 | { 10 | [Command("up", "Bring a database up to date")] 11 | class UpdateCommand : Command 12 | { 13 | readonly DefineVariablesFeature _defineVariablesFeature; 14 | readonly UsernamePasswordFeature _usernamePasswordFeature; 15 | readonly DatabaseFeature _databaseFeature; 16 | readonly LoggingFeature _loggingFeature; 17 | readonly ScriptRootFeature _scriptRootFeature; 18 | 19 | bool _createIfMissing = true; 20 | 21 | public UpdateCommand() 22 | { 23 | _databaseFeature = Enable(); 24 | _usernamePasswordFeature = Enable(); 25 | _scriptRootFeature = Enable(); 26 | _defineVariablesFeature = Enable(); 27 | 28 | Options.Add("no-create", "If the database does not already exist, do not attempt to create it", _ => _createIfMissing = false); 29 | 30 | _loggingFeature = Enable(); 31 | } 32 | 33 | protected override int Run() 34 | { 35 | _loggingFeature.Configure(); 36 | 37 | try 38 | { 39 | using var connection = DatabaseConnector.Connect(_databaseFeature.Host!, _databaseFeature.Database!, _usernamePasswordFeature.Username!, _usernamePasswordFeature.Password!, _createIfMissing); 40 | UpdateSession.ApplyChangeScripts(connection, _scriptRootFeature.ScriptRoot!, _defineVariablesFeature.Variables); 41 | 42 | return 0; 43 | } 44 | catch (PostgresException ex) 45 | { 46 | Log.Fatal("Could not apply change scripts: {Message} ({SqlState})", ex.MessageText, ex.SqlState); 47 | return -1; 48 | } 49 | catch (Exception ex) 50 | { 51 | Log.Fatal(ex, "Could not apply change scripts"); 52 | return -1; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Commands/HelpCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Autofac.Features.Metadata; 6 | 7 | namespace Datalust.Piggy.Cli.Commands 8 | { 9 | [Command("help", "Show information about available commands")] 10 | class HelpCommand : Command 11 | { 12 | readonly List, CommandMetadata>> _availableCommands; 13 | 14 | public HelpCommand(IEnumerable, CommandMetadata>> availableCommands) 15 | { 16 | _availableCommands = availableCommands.OrderBy(c => c.Metadata.Name).ToList(); 17 | } 18 | 19 | protected override int Run(string[] unrecognised) 20 | { 21 | var ea = Assembly.GetEntryAssembly()!; 22 | var name = ea.GetName().Name; 23 | 24 | if (unrecognised.Length > 0) 25 | { 26 | var target = unrecognised[0].ToLowerInvariant(); 27 | var cmd = _availableCommands.SingleOrDefault(c => c.Metadata.Name == target); 28 | if (cmd != null) 29 | { 30 | var argHelp = cmd.Value.Value.HasArgs ? " []" : ""; 31 | Console.WriteLine(name + " " + cmd.Metadata.Name + argHelp); 32 | Console.WriteLine(); 33 | Console.WriteLine(cmd.Metadata.HelpText); 34 | Console.WriteLine(); 35 | 36 | cmd.Value.Value.PrintUsage(); 37 | return 0; 38 | } 39 | 40 | base.Run(unrecognised); 41 | } 42 | 43 | Console.WriteLine($"Usage: {name} []"); 44 | Console.WriteLine(); 45 | Console.WriteLine("Available commands are:"); 46 | 47 | foreach (var availableCommand in _availableCommands) 48 | { 49 | Printing.Define( 50 | " " + availableCommand.Metadata.Name, 51 | availableCommand.Metadata.HelpText!, 52 | 13, 53 | Console.Out); 54 | } 55 | 56 | Console.WriteLine(); 57 | Console.WriteLine($"Type `{name} help ` for detailed help"); 58 | 59 | return 0; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Datalust.Piggy/Datalust.Piggy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A friendly PostgreSQL script runner in the spirit of DbUp. 5 | Exe 6 | 2.0.1 7 | net8.0 8 | piggy 9 | ..\..\asset\Piggy-Icon-128px.ico 10 | win-x64;linux-x64;osx-x64 11 | true 12 | Datalust and Contributors 13 | Datalust.Piggy 14 | postgresql 15 | https://github.com/datalust/piggy 16 | Apache-2.0 17 | Piggy-Icon-128px.png 18 | true 19 | enable 20 | latest 21 | partial 22 | true 23 | README.md 24 | 25 | 26 | 27 | Datalust.Piggy.Cli 28 | piggy 29 | Major 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Update/UpdateSession.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | using Datalust.Piggy.Filesystem; 5 | using Datalust.Piggy.History; 6 | using Datalust.Piggy.Status; 7 | using Datalust.Piggy.Syntax; 8 | using Npgsql; 9 | using Serilog; 10 | 11 | namespace Datalust.Piggy.Update 12 | { 13 | /// 14 | /// Applies updates to a target database. 15 | /// 16 | public static class UpdateSession 17 | { 18 | /// 19 | /// Apply change scripts from a folder hierarchy. 20 | /// 21 | /// The database connection to use. 22 | /// A root filesystem folder under which change scripts are stored. 23 | /// A set of variables to replace within change scripts. 24 | public static void ApplyChangeScripts(NpgsqlConnection connection, string scriptRoot, IReadOnlyDictionary variables) 25 | { 26 | var scripts = DatabaseStatus.GetPendingScripts(connection, scriptRoot); 27 | 28 | Log.Information("Found {Count} new script files to apply", scripts.Length); 29 | 30 | if (scripts.Length != 0) 31 | { 32 | Log.Information("Ensuring the change log table exists"); 33 | using (var command = new NpgsqlCommand(AppliedChangeScriptLog.ChangesTableCreateScript, connection)) 34 | command.ExecuteNonQuery(); 35 | } 36 | 37 | foreach (var script in scripts) 38 | { 39 | Log.Information("Applying {FullPath} as {ScriptFile}", script.FullPath, script.RelativeName); 40 | ApplyChangeScript(connection, script, variables); 41 | } 42 | 43 | Log.Information("Done"); 44 | } 45 | 46 | static void ApplyChangeScript(NpgsqlConnection connection, ChangeScriptFile script, IReadOnlyDictionary variables) 47 | { 48 | var content = File.ReadAllText(script.FullPath); 49 | var directives = ChangeScriptDirectiveParser.Parse(content); 50 | 51 | var commandText = new StringBuilder(); 52 | if (directives.IsTransacted) 53 | commandText.AppendLine("START TRANSACTION ISOLATION LEVEL REPEATABLE READ;"); 54 | 55 | commandText.AppendLine(ChangeScriptVariableProcessor.InsertVariables(content, variables)); 56 | commandText.AppendLine(";"); 57 | 58 | commandText.AppendLine(AppliedChangeScriptLog.CreateApplyLogScriptFor(script)); 59 | 60 | if (directives.IsTransacted) 61 | commandText.AppendLine("COMMIT TRANSACTION;"); 62 | 63 | using (var command = new NpgsqlCommand(commandText.ToString(), connection)) 64 | { 65 | command.ExecuteNonQuery(); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/Datalust.Piggy.Tests/Database/ConnectionStringParserTests.cs: -------------------------------------------------------------------------------- 1 | using Datalust.Piggy.Database; 2 | using Xunit; 3 | 4 | namespace Datalust.Piggy.Tests.Database 5 | { 6 | public class ConnectionStringParserTests 7 | { 8 | [Fact] 9 | public void ParsesGoodConnectionStringCorrectly1() 10 | { 11 | const string connectionString = "Host=localhost"; 12 | var parts = ConnectionStringParser.Parse(connectionString); 13 | Assert.Equal("localhost", parts["Host"]); 14 | } 15 | 16 | [Fact] 17 | public void ParsesGoodConnectionStringCorrectly2() 18 | { 19 | const string connectionString = "Host=localhost;Username=hunter2;Password=KenSentMe;Database=Skynet"; 20 | var parts = ConnectionStringParser.Parse(connectionString); 21 | Assert.Equal("localhost", parts["Host"]); 22 | Assert.Equal("hunter2", parts["Username"]); 23 | Assert.Equal("KenSentMe", parts["Password"]); 24 | Assert.Equal("Skynet", parts["Database"]); 25 | } 26 | 27 | [Fact] 28 | public void ParserHandlesDuplicateConnectionStringKeysCorrectly() 29 | { 30 | const string connectionString = "Host=localhost;Host=127.0.0.1"; 31 | var parts = ConnectionStringParser.Parse(connectionString); 32 | Assert.Equal("localhost", parts["Host"]); 33 | } 34 | 35 | [Fact] 36 | public void ParserHandlesDuplicateButDifferentCaseConnectionStringKeysCorrectly() 37 | { 38 | const string connectionString = "Host=localhost;host=127.0.0.1"; 39 | var parts = ConnectionStringParser.Parse(connectionString); 40 | Assert.Equal("localhost", parts["Host"]); 41 | Assert.Equal("127.0.0.1", parts["host"]); 42 | } 43 | 44 | [Theory] 45 | [InlineData(null)] 46 | [InlineData("")] 47 | [InlineData(" ")] 48 | [InlineData(";")] 49 | [InlineData("=;")] 50 | [InlineData("==;")] 51 | [InlineData("=;;")] 52 | [InlineData("=;;=")] 53 | [InlineData("=;=;=")] 54 | [InlineData("Host")] 55 | [InlineData("Host=")] 56 | [InlineData(";Host=")] 57 | [InlineData("Host=;")] 58 | [InlineData("localhost")] 59 | [InlineData("=localhost")] 60 | [InlineData(";=localhost")] 61 | [InlineData("=localhost;")] 62 | [InlineData("Host=;;")] 63 | [InlineData("Host=;=;")] 64 | [InlineData("Host=;Username=;")] 65 | [InlineData("Host=;=hunter2;")] 66 | [InlineData("=localhost;Username=;")] 67 | [InlineData("=localhost;=hunter2;")] 68 | [InlineData(";Username=;=KenSentMe;DatabaseSkynet")] 69 | public void DoesNotParseBadConnectionStrings(string? connectionString) 70 | { 71 | var parts = ConnectionStringParser.Parse(connectionString); 72 | Assert.Empty(parts); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Datalust.Piggy.Cli 6 | { 7 | abstract class Command 8 | { 9 | readonly IList _features = new List(); 10 | 11 | protected Command() 12 | { 13 | Options = new OptionSet(); 14 | } 15 | 16 | protected OptionSet Options { get; } 17 | 18 | public bool HasArgs => Options.Any(); 19 | 20 | protected T Enable() 21 | where T : CommandFeature, new() 22 | { 23 | var t = new T(); 24 | return Enable(t); 25 | } 26 | 27 | protected T Enable(T t) 28 | where T : CommandFeature 29 | { 30 | if (t == null) throw new ArgumentNullException(nameof(t)); 31 | t.Enable(Options); 32 | _features.Add(t); 33 | return t; 34 | } 35 | 36 | public void PrintUsage() 37 | { 38 | if (Options.Any()) 39 | { 40 | Console.Error.WriteLine("Arguments:"); 41 | Options.WriteOptionDescriptions(Console.Error); 42 | } 43 | } 44 | 45 | public int Invoke(string[] args) 46 | { 47 | try 48 | { 49 | var unrecognised = Options.Parse(args).ToArray(); 50 | 51 | var errs = _features.SelectMany(f => f.GetUsageErrors()).ToList(); 52 | 53 | if (errs.Any()) 54 | { 55 | ShowUsageErrors(errs); 56 | return -1; 57 | } 58 | 59 | return Run(unrecognised); 60 | } 61 | catch (Exception ex) 62 | { 63 | Console.Error.WriteLine(ex.Message); 64 | return -1; 65 | } 66 | } 67 | 68 | protected virtual int Run(string[] unrecognised) 69 | { 70 | if (unrecognised.Any()) 71 | { 72 | ShowUsageErrors(new [] { "Unrecognized options: " + string.Join(", ", unrecognised) }); 73 | return -1; 74 | } 75 | 76 | return Run(); 77 | } 78 | 79 | protected virtual int Run() { return 0; } 80 | 81 | protected virtual void ShowUsageErrors(IEnumerable errors) 82 | { 83 | var header = "Error:"; 84 | foreach (var error in errors) 85 | { 86 | Printing.Define(header, error, 7, Console.Out); 87 | header = new string(' ', header.Length); 88 | } 89 | } 90 | 91 | protected bool Require(string? value, string name) 92 | { 93 | if (!Requirement.IsNonEmpty(value)) 94 | { 95 | ShowUsageErrors(new [] { Requirement.NonEmptyDescription(name) }); 96 | return false; 97 | } 98 | return true; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /piggy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29020.237 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FC0A256C-CC1F-4ECE-AF09-707248D10DC1}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sln", "sln", "{2EA56595-519F-4DD5-9E94-CCC43E3DF624}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | appveyor.yml = appveyor.yml 12 | Build.ps1 = Build.ps1 13 | LICENSE = LICENSE 14 | README.md = README.md 15 | global.json = global.json 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3587B633-0C03-4235-8903-6226900328F1}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datalust.Piggy", "src\Datalust.Piggy\Datalust.Piggy.csproj", "{8250E09A-7479-4322-A251-DABFD9F129B6}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datalust.Piggy.Tests", "test\Datalust.Piggy.Tests\Datalust.Piggy.Tests.csproj", "{E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "asset", "asset", "{438A0DA5-F3CF-4FCE-B43A-B6DA2981D4AE}" 25 | ProjectSection(SolutionItems) = preProject 26 | asset\Piggy-400px.png = asset\Piggy-400px.png 27 | asset\Piggy-Icon-128px.ico = asset\Piggy-Icon-128px.ico 28 | asset\Piggy-Icon-128px.png = asset\Piggy-Icon-128px.png 29 | asset\Piggy.svg = asset\Piggy.svg 30 | asset\PiggyLicense.rtf = asset\PiggyLicense.rtf 31 | EndProjectSection 32 | EndProject 33 | Global 34 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 35 | Debug|Any CPU = Debug|Any CPU 36 | Debug|x64 = Debug|x64 37 | Release|Any CPU = Release|Any CPU 38 | Release|x64 = Release|x64 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Debug|x64.ActiveCfg = Debug|Any CPU 44 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Debug|x64.Build.0 = Debug|Any CPU 45 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Release|x64.ActiveCfg = Release|Any CPU 48 | {8250E09A-7479-4322-A251-DABFD9F129B6}.Release|x64.Build.0 = Release|Any CPU 49 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Debug|x64.Build.0 = Debug|Any CPU 53 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Release|x64.ActiveCfg = Release|Any CPU 56 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5}.Release|x64.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(NestedProjects) = preSolution 62 | {8250E09A-7479-4322-A251-DABFD9F129B6} = {FC0A256C-CC1F-4ECE-AF09-707248D10DC1} 63 | {E3291CB6-F4A9-4A01-9E65-B70E2A32B0D5} = {3587B633-0C03-4235-8903-6226900328F1} 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {6D299713-28D4-4078-9DA7-9033679CD200} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Database/DatabaseConnector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Npgsql; 3 | using Serilog; 4 | using Serilog.Extensions.Logging; 5 | 6 | namespace Datalust.Piggy.Database; 7 | 8 | /// 9 | /// Assists with making a PostgreSQL database connection. 10 | /// 11 | public static class DatabaseConnector 12 | { 13 | static DatabaseConnector() 14 | { 15 | NpgsqlLoggingConfiguration.InitializeLogging(new SerilogLoggerFactory()); 16 | } 17 | 18 | /// 19 | /// Connect to the specified database. 20 | /// 21 | /// The PostgreSQL host to connect to. 22 | /// The database to update. 23 | /// The username to authenticate with. 24 | /// The password to authenticate with. 25 | /// If true, Piggy will attempt to create the database if it doesn't exist. The 26 | /// database must already exist, otherwise. 27 | /// An open database connection. 28 | public static NpgsqlConnection Connect(string host, string database, string username, string password, bool createIfMissing) 29 | { 30 | try 31 | { 32 | return Connect($"Host={host};Username={username};Password={password};Database={database}"); 33 | } 34 | catch (PostgresException px) when (px.SqlState == "3D000") 35 | { 36 | if (createIfMissing && TryCreate(host, database, username, password)) 37 | return Connect(host, database, username, password, false); 38 | 39 | throw; 40 | } 41 | } 42 | 43 | /// 44 | /// Connect to the specified database. 45 | /// 46 | /// A connection string identifying the database. 47 | /// An open database connection. 48 | public static NpgsqlConnection Connect(string connectionString) 49 | { 50 | if (connectionString == null) throw new ArgumentNullException(nameof(connectionString)); 51 | 52 | var conn = new NpgsqlConnection(connectionString); 53 | 54 | var host = conn.Host; 55 | if (conn.Host == null && !string.IsNullOrWhiteSpace(conn.ConnectionString)) 56 | { 57 | var parts = ConnectionStringParser.Parse(conn.ConnectionString); 58 | if (parts.TryGetValue("Host", out var value)) host = value; 59 | } 60 | 61 | Log.Information("Connecting to database {Database} on {Host}", conn.Database, host); 62 | 63 | try 64 | { 65 | conn.Open(); 66 | } 67 | catch 68 | { 69 | conn.Dispose(); 70 | throw; 71 | } 72 | 73 | Log.Information("Connected"); 74 | 75 | return conn; 76 | } 77 | 78 | static bool TryCreate(string host, string database, string username, string password) 79 | { 80 | Log.Information("Database does not exist; attempting to create it"); 81 | 82 | var postgresCstr = $"Host={host};Username={username};Password={password};Database=postgres"; 83 | using (var postgresConn = new NpgsqlConnection(postgresCstr)) 84 | { 85 | postgresConn.Open(); 86 | 87 | Log.Information("Creating database {Database} on {Host} with owner {Owner} and default options", database, host, username); 88 | using (var createCommand = new NpgsqlCommand($"CREATE DATABASE {database} WITH OWNER = {username} ENCODING = 'UTF8' CONNECTION LIMIT = -1;", postgresConn)) 89 | { 90 | createCommand.ExecuteNonQuery(); 91 | Log.Information("Database created successfully"); 92 | return true; 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Piggy](https://raw.githubusercontent.com/datalust/piggy/master/asset/Piggy-400px.png) 2 | 3 | A friendly PostgreSQL script runner in the spirit of [DbUp](https://github.com/DbUp/DbUp). 4 | 5 | [![Build status](https://ci.appveyor.com/api/projects/status/889gkdpvjbjuhkfg?svg=true)](https://ci.appveyor.com/project/datalust/piggy) 6 | 7 | ### What is Piggy? 8 | 9 | Piggy is a simple command-line tool for managing schema and data changes to PostgreSQL databases. Piggy looks for `.sql` files in a directory and applies them to the database in order, using transactions and a change log table to ensure each script runs only once per database. 10 | 11 | ### Installation 12 | 13 | Piggy is available as a .NET tool package, as standalone binaries, and as a C♯ library that can be used directly in .NET applications. 14 | 15 | #### `dotnet tool` 16 | 17 | Piggy is distributed as a [.NET tool package called _Datalust.Piggy.Cli_](https://nuget.org/packages/datalust.piggy.cli), which can be installed using: 18 | 19 | ``` 20 | dotnet tool install --global Datalust.Piggy.Cli 21 | ``` 22 | 23 | The executable is called `piggy`. Test that the installation was successful with: 24 | 25 | ``` 26 | piggy --version 27 | ``` 28 | 29 | #### Executable binaries 30 | 31 | Pre-built, native, self-contained Piggy binaries are available for Windows, macOS and Linux from [the releases page](https://github.com/datalust/piggy/releases). If your platform of choice isn't listed, please [raise an issue here](https://github.com/datalust/piggy/issues) so that we can add it. 32 | 33 | #### C# library 34 | 35 | For development and test automation purposes, the core script runner is also packaged as a C♯ API and [published to NuGet as _Datalust.Piggy_](https://nuget.org/packages/datalust.piggy). 36 | 37 | ```csharp 38 | // dotnet add package Datalust.Piggy 39 | var connectionString = // Npgsql connection string 40 | using (var connection = DatabaseConnector.Connect(connectionString)) 41 | { 42 | UpdateSession.ApplyChangeScripts(connection, "./db", new Dictionary()); 43 | } 44 | ``` 45 | 46 | ### Workflow 47 | 48 | To manage database updates with Piggy, write SQL scripts to make changes, rather than applying changes directly. 49 | 50 | Organize change scripts on the file system under a _script root_ directory. Name files so that they sort lexicographically in the order in which they need to be executed: 51 | 52 | ``` 53 | 001-create-schema.sql 54 | 002-create-users-table.sql 55 | 003-... 56 | ``` 57 | 58 | If the scripts are arranged in subdirectories, these must be ordered by name as well: 59 | 60 | ``` 61 | v1/ 62 | 001-create-schema.sql 63 | 002-create-users-table.sql 64 | 003-... 65 | v2/ 66 | 001-rename-users-table.sql 67 | ``` 68 | 69 | Piggy enumerates `.sql` files and generates names like `/v1/001-create-schema.sql` using each script's filename relative to the script root. These relative names are checked against the change log table to determine which of them need to be run. 70 | 71 | To bring a database up to date, run `piggy up`, providing the script root, host, database name, username and password: 72 | 73 | ``` 74 | piggy up -s -h -d -u -p 75 | ``` 76 | 77 | If the database does not exist, Piggy will create it using sensible defaults. To opt out of this behavior, add the `--no-create` flag. 78 | 79 | Over time, as your application grows, create new scripts to move the database forwards - don't edit the existing ones, since they've already been applied and will be ignored by Piggy. 80 | 81 | Piggy can be used to update from any previous schema version to the current one: scripts that have already been run on a database are ignored, so only necessary scripts are applied. 82 | 83 | For more detailed usage information, run `piggy help up`; to see all available commands run `piggy help`. 84 | 85 | ### Transactions 86 | 87 | Piggy wraps each script in a transaction that also covers the change log table update. A few DDL statements can't run within a transaction in PostgreSQL - in these cases, add: 88 | 89 | ```sql 90 | -- PIGGY NO TRANSACTION 91 | ``` 92 | 93 | as the first line of the script. 94 | 95 | ### Variable substitution 96 | 97 | Piggy uses `$var$` syntax for replaced variables: 98 | 99 | ```sql 100 | create table $schema$.users (name varchar(140) not null); 101 | insert into $schema$.users (name) values ('$admin$'); 102 | ``` 103 | 104 | Values are inserted using pure text substitution: no escaping or other processing is applied. If no value is supplied for a variable that appears in a script, Piggy will leave the script unchanged (undefined variables will not be replaced with the empty string). 105 | 106 | Variables are supplied on the command-line with the `-v` flag: 107 | 108 | ``` 109 | piggy up ... -v schema=myapp -v admin=myuser 110 | ``` 111 | 112 | ### Change log 113 | 114 | The change log is stored in the target database in `piggy.changes`. The `piggy log` command is used to view applied change scripts. 115 | 116 | ### Help 117 | 118 | Run `piggy help` to see all available commands, and `piggy help ` for detailed command help. 119 | 120 | ``` 121 | > piggy help 122 | Usage: piggy [] 123 | 124 | Available commands are: 125 | baseline Add scripts to the change log without running them 126 | help Show information about available commands 127 | log List change scripts that have been applied to a database 128 | pending Determine which scripts will be run in an update 129 | up Bring a database up to date 130 | --version Print the current executable version 131 | 132 | Type `piggy help ` for detailed help 133 | ``` 134 | -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $tfm = "net8.0" 4 | 5 | Write-Output "build: Build started" 6 | Push-Location $PSScriptRoot 7 | 8 | Write-Output "build: Loading sdk.version from global.json" 9 | $json = Get-Content 'global.json' | Out-String | ConvertFrom-Json 10 | $requiredSdkVersion = $json.sdk.version 11 | Write-Output "build: Required sdk.version is $requiredSdkVersion" 12 | 13 | Write-Output "build: Downloading .NET SDK $requiredSdkVersion" 14 | Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile 'dotnet-install.ps1' 15 | 16 | Write-Output "build: Installing .NET SDK $requiredSdkVersion" 17 | ./dotnet-install.ps1 -Version $requiredSdkVersion 18 | 19 | Write-Output "build: Checking installed .NET SDK versions" 20 | $installedSdkVersions = & dotnet --list-sdks 21 | Write-Output "build: Installed .NET SDK versions:" 22 | Write-Output $installedSdkVersions 23 | 24 | if($installedSdkVersions -inotmatch $requiredSdkVersion) { 25 | Write-Output "build: Did not find .NET SDK $requiredSdkVersion installed" 26 | Write-Output $installedSdkVersions 27 | exit 1 28 | } 29 | Write-Output "build: .NET SDK $requiredSdkVersion is installed and available for use" 30 | 31 | $artifacts = ".artifacts" 32 | if(Test-Path .\$artifacts) { 33 | Write-Output "build: Cleaning .\$artifacts" 34 | Remove-Item .\$artifacts -Force -Recurse 35 | } 36 | 37 | Write-Output "build: Restoring .NET packages (--no-cache)" 38 | & dotnet restore --no-cache 39 | if($LASTEXITCODE -ne 0) { exit 2 } 40 | 41 | Write-Output "build: Checking for vulnerable packages..." 42 | & dotnet list package --vulnerable 43 | if($LASTEXITCODE -ne 0) { exit 3 } 44 | 45 | Write-Output "Checking for deprecated packages..." 46 | & dotnet list package --deprecated 47 | if($LASTEXITCODE -ne 0) { exit 4 } 48 | 49 | Write-Output "Checking for outdated packages..." 50 | & dotnet list package --outdated 51 | if($LASTEXITCODE -ne 0) { exit 5 } 52 | 53 | Write-Output "build: Testing project" 54 | & dotnet test ./test/Datalust.Piggy.Tests/Datalust.Piggy.Tests.csproj -c Release 55 | if($LASTEXITCODE -ne 0) { exit 6 } 56 | 57 | $projectDir = "src/Datalust.Piggy/" 58 | $projectFile = "Datalust.Piggy.csproj" 59 | $project = $projectDir + $projectFile 60 | Write-Output "build: Project .csproj file is located at $project" 61 | 62 | Write-Output "build: Getting Version from $project" 63 | $xml = [Xml](Get-Content $project) 64 | $prefix = [Version]$xml.Project.PropertyGroup[0].VersionPrefix.ToString() 65 | Write-Output "build: Version prefix is $prefix" 66 | 67 | Write-Output "build: Calculating `$branch, `$revision, and `$suffix" 68 | $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:APPVEYOR_REPO_BRANCH]; 69 | Write-Output "build: `$branch is $branch" 70 | $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:APPVEYOR_BUILD_NUMBER]; 71 | Write-Output "build: `$revision is $revision" 72 | $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] 73 | Write-Output "build: `$suffix is $suffix" 74 | 75 | $version= "$prefix-$suffix" 76 | Write-Output "build: Version is $version" 77 | 78 | Write-Output "build: Creating artifacts directory at .\$artifacts" 79 | New-Item -ItemType "directory" -Name $artifacts 80 | 81 | Write-Output "build: Packing library into Datalust.Piggy.*.nupkg" 82 | if ($suffix) { 83 | & dotnet pack $project -c Release -o $PSScriptRoot/$artifacts /p:OutputType=Library --version-suffix=$suffix 84 | } else { 85 | & dotnet pack $project -c Release -o $PSScriptRoot/$artifacts /p:OutputType=Library 86 | } 87 | if($LASTEXITCODE -ne 0) { exit 11 } 88 | 89 | Write-Output "build: Packing executable into dotnet tool Datalust.Piggy.Cli.*.nupkg" 90 | if ($suffix) { 91 | & dotnet build $project --no-incremental -c Release -o $PSScriptRoot/$artifacts /p:PackAsTool=True --version-suffix=$suffix 92 | if($LASTEXITCODE -ne 0) { exit 11 } 93 | & dotnet pack $project -c Release -o $PSScriptRoot/$artifacts /p:PackAsTool=True --version-suffix=$suffix 94 | } else { 95 | & dotnet build $project --no-incremental -c Release -o $PSScriptRoot/$artifacts /p:PackAsTool=True 96 | if($LASTEXITCODE -ne 0) { exit 11 } 97 | & dotnet pack $project -c Release -o $PSScriptRoot/$artifacts /p:PackAsTool=True 98 | } 99 | if($LASTEXITCODE -ne 0) { exit 11 } 100 | 101 | $rids = @("win-x64", "linux-x64", "osx-x64", "osx-arm64") 102 | foreach ($rid in $rids) { 103 | Write-Output "build: Building a $rid build of version $version" 104 | 105 | if ($suffix) { 106 | & dotnet publish $project -c Release -f $tfm -r $rid /p:PublishSingleFile=true /p:SelfContained=true /p:PublishTrimmed=true --version-suffix=$suffix 107 | } else { 108 | & dotnet publish $project -c Release -f $tfm -r $rid /p:PublishSingleFile=true /p:SelfContained=true /p:PublishTrimmed=true 109 | } 110 | if($LASTEXITCODE -ne 0) { exit 7 } 111 | 112 | # Make sure the archive contains a reasonable root filename. 113 | Move-Item ./src/Datalust.Piggy/bin/Release/$tfm/$rid/publish/ ./src/Datalust.Piggy/bin/Release/$tfm/$rid/piggy-$version-$rid/ 114 | 115 | if ($rid -ne "win-x64") { 116 | & ./build/7-zip/7za.exe a -ttar piggy-$version-$rid.tar ./src/Datalust.Piggy/bin/Release/$tfm/$rid/piggy-$version-$rid/ 117 | if($LASTEXITCODE -ne 0) { exit 8 } 118 | 119 | & ./build/7-zip/7za.exe a -tgzip ./$artifacts/piggy-$version-$rid.tar.gz piggy-$version-$rid.tar 120 | if($LASTEXITCODE -ne 0) { exit 9 } 121 | 122 | Remove-Item piggy-$version-$rid.tar 123 | } else { 124 | & ./build/7-zip/7za.exe a -tzip ./$artifacts/piggy-$version-$rid.zip ./src/Datalust.Piggy/bin/Release/$tfm/$rid/piggy-$version-$rid/ 125 | if($LASTEXITCODE -ne 0) { exit 10 } 126 | } 127 | 128 | # Move back to the original directory name. 129 | Move-Item ./src/Datalust.Piggy/bin/Release/$tfm/$rid/piggy-$version-$rid/ ./src/Datalust.Piggy/bin/Release/$tfm/$rid/publish/ 130 | } 131 | 132 | Pop-Location 133 | Write-Output "build: completed successfully" 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | .artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | 290 | # ignore downloaded dotnet-install.ps1 used in .\Build.ps1 291 | dotnet-install.ps1 292 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Attribution/Serilog License.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /asset/Piggy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 54 | 58 | 63 | 68 | 73 | 79 | 84 | 85 | 87 | 92 | 98 | 99 | 104 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/Datalust.Piggy/Cli/Options.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Options.cs 3 | // 4 | // Authors: 5 | // Jonathan Pryor 6 | // Federico Di Gregorio 7 | // Rolf Bjarne Kvinge 8 | // 9 | // Copyright (C) 2008 Novell (http://www.novell.com) 10 | // Copyright (C) 2009 Federico Di Gregorio. 11 | // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com) 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining 14 | // a copy of this software and associated documentation files (the 15 | // "Software"), to deal in the Software without restriction, including 16 | // without limitation the rights to use, copy, modify, merge, publish, 17 | // distribute, sublicense, and/or sell copies of the Software, and to 18 | // permit persons to whom the Software is furnished to do so, subject to 19 | // the following conditions: 20 | // 21 | // The above copyright notice and this permission notice shall be 22 | // included in all copies or substantial portions of the Software. 23 | // 24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 27 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 28 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 29 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 30 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | // 32 | 33 | #nullable disable 34 | 35 | // Compile With: 36 | // gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll 37 | // gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll 38 | // 39 | // The LINQ version just changes the implementation of 40 | // OptionSet.Parse(IEnumerable), and confers no semantic changes. 41 | 42 | // 43 | // A Getopt::Long-inspired option parsing library for C#. 44 | // 45 | // NDesk.Options.OptionSet is built upon a key/value table, where the 46 | // key is a option format string and the value is a delegate that is 47 | // invoked when the format string is matched. 48 | // 49 | // Option format strings: 50 | // Regex-like BNF Grammar: 51 | // name: .+ 52 | // type: [=:] 53 | // sep: ( [^{}]+ | '{' .+ '}' )? 54 | // aliases: ( name type sep ) ( '|' name type sep )* 55 | // 56 | // Each '|'-delimited name is an alias for the associated action. If the 57 | // format string ends in a '=', it has a required value. If the format 58 | // string ends in a ':', it has an optional value. If neither '=' or ':' 59 | // is present, no value is supported. `=' or `:' need only be defined on one 60 | // alias, but if they are provided on more than one they must be consistent. 61 | // 62 | // Each alias portion may also end with a "key/value separator", which is used 63 | // to split option values if the option accepts > 1 value. If not specified, 64 | // it defaults to '=' and ':'. If specified, it can be any character except 65 | // '{' and '}' OR the *string* between '{' and '}'. If no separator should be 66 | // used (i.e. the separate values should be distinct arguments), then "{}" 67 | // should be used as the separator. 68 | // 69 | // Options are extracted either from the current option by looking for 70 | // the option name followed by an '=' or ':', or is taken from the 71 | // following option IFF: 72 | // - The current option does not contain a '=' or a ':' 73 | // - The current option requires a value (i.e. not a Option type of ':') 74 | // 75 | // The `name' used in the option format string does NOT include any leading 76 | // option indicator, such as '-', '--', or '/'. All three of these are 77 | // permitted/required on any named option. 78 | // 79 | // Option bundling is permitted so long as: 80 | // - '-' is used to start the option group 81 | // - all of the bundled options are a single character 82 | // - at most one of the bundled options accepts a value, and the value 83 | // provided starts from the next character to the end of the string. 84 | // 85 | // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' 86 | // as '-Dname=value'. 87 | // 88 | // Option processing is disabled by specifying "--". All options after "--" 89 | // are returned by OptionSet.Parse() unchanged and unprocessed. 90 | // 91 | // Unprocessed options are returned from OptionSet.Parse(). 92 | // 93 | // Examples: 94 | // int verbose = 0; 95 | // OptionSet p = new OptionSet () 96 | // .Add ("v", v => ++verbose) 97 | // .Add ("name=|value=", v => Console.WriteLine (v)); 98 | // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); 99 | // 100 | // The above would parse the argument string array, and would invoke the 101 | // lambda expression three times, setting `verbose' to 3 when complete. 102 | // It would also print out "A" and "B" to standard output. 103 | // The returned array would contain the string "extra". 104 | // 105 | // C# 3.0 collection initializers are supported and encouraged: 106 | // var p = new OptionSet () { 107 | // { "h|?|help", v => ShowHelp () }, 108 | // }; 109 | // 110 | // System.ComponentModel.TypeConverter is also supported, allowing the use of 111 | // custom data types in the callback type; TypeConverter.ConvertFromString() 112 | // is used to convert the value option to an instance of the specified 113 | // type: 114 | // 115 | // var p = new OptionSet () { 116 | // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, 117 | // }; 118 | // 119 | // Random other tidbits: 120 | // - Boolean options (those w/o '=' or ':' in the option format string) 121 | // are explicitly enabled if they are followed with '+', and explicitly 122 | // disabled if they are followed with '-': 123 | // string a = null; 124 | // var p = new OptionSet () { 125 | // { "a", s => a = s }, 126 | // }; 127 | // p.Parse (new string[]{"-a"}); // sets v != null 128 | // p.Parse (new string[]{"-a+"}); // sets v != null 129 | // p.Parse (new string[]{"-a-"}); // sets v == null 130 | // 131 | 132 | using System; 133 | using System.Collections; 134 | using System.Collections.Generic; 135 | using System.Collections.ObjectModel; 136 | using System.IO; 137 | using System.Reflection; 138 | using System.Text; 139 | using System.Text.RegularExpressions; 140 | 141 | #if LINQ 142 | using System.Linq; 143 | #endif 144 | 145 | #if TEST 146 | using NDesk.Options; 147 | #endif 148 | 149 | #if NDESK_OPTIONS 150 | namespace NDesk.Options 151 | #else 152 | namespace Datalust.Piggy.Cli 153 | #endif 154 | { 155 | delegate U Converter(T t); 156 | 157 | static class StringCoda { 158 | 159 | public static IEnumerable WrappedLines (string self, params int[] widths) 160 | { 161 | IEnumerable w = widths; 162 | return WrappedLines (self, w); 163 | } 164 | 165 | public static IEnumerable WrappedLines (string self, IEnumerable widths) 166 | { 167 | if (widths == null) 168 | throw new ArgumentNullException (nameof(widths)); 169 | return CreateWrappedLinesIterator (self, widths); 170 | } 171 | 172 | private static IEnumerable CreateWrappedLinesIterator (string self, IEnumerable widths) 173 | { 174 | if (string.IsNullOrEmpty (self)) { 175 | yield return string.Empty; 176 | yield break; 177 | } 178 | using (IEnumerator ewidths = widths.GetEnumerator ()) { 179 | bool? hw = null; 180 | int width = GetNextWidth (ewidths, int.MaxValue, ref hw); 181 | int start = 0, end; 182 | do { 183 | end = GetLineEnd (start, width, self); 184 | char c = self [end-1]; 185 | if (char.IsWhiteSpace (c)) 186 | --end; 187 | bool needContinuation = end != self.Length && !IsEolChar (c); 188 | string continuation = ""; 189 | if (needContinuation) { 190 | --end; 191 | continuation = "-"; 192 | } 193 | string line = self.Substring (start, end - start) + continuation; 194 | yield return line; 195 | start = end; 196 | if (char.IsWhiteSpace (c)) 197 | ++start; 198 | width = GetNextWidth (ewidths, width, ref hw); 199 | } while (start < self.Length); 200 | } 201 | } 202 | 203 | private static int GetNextWidth (IEnumerator ewidths, int curWidth, ref bool? eValid) 204 | { 205 | if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) { 206 | curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth; 207 | // '.' is any character, - is for a continuation 208 | const string minWidth = ".-"; 209 | if (curWidth < minWidth.Length) 210 | throw new ArgumentOutOfRangeException ("widths", 211 | string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth)); 212 | return curWidth; 213 | } 214 | // no more elements, use the last element. 215 | return curWidth; 216 | } 217 | 218 | private static bool IsEolChar (char c) 219 | { 220 | return !char.IsLetterOrDigit (c); 221 | } 222 | 223 | private static int GetLineEnd (int start, int length, string description) 224 | { 225 | int end = System.Math.Min (start + length, description.Length); 226 | int sep = -1; 227 | for (int i = start; i < end; ++i) { 228 | if (description [i] == '\n') 229 | return i+1; 230 | if (IsEolChar (description [i])) 231 | sep = i+1; 232 | } 233 | if (sep == -1 || end == description.Length) 234 | return end; 235 | return sep; 236 | } 237 | } 238 | 239 | class OptionValueCollection : IList, IList { 240 | 241 | List values = new List (); 242 | OptionContext c; 243 | 244 | internal OptionValueCollection (OptionContext c) 245 | { 246 | this.c = c; 247 | } 248 | 249 | #region ICollection 250 | void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);} 251 | bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}} 252 | object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}} 253 | #endregion 254 | 255 | #region ICollection 256 | public void Add (string item) {values.Add (item);} 257 | public void Clear () {values.Clear ();} 258 | public bool Contains (string item) {return values.Contains (item);} 259 | public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);} 260 | public bool Remove (string item) {return values.Remove (item);} 261 | public int Count {get {return values.Count;}} 262 | public bool IsReadOnly {get {return false;}} 263 | #endregion 264 | 265 | #region IEnumerable 266 | IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();} 267 | #endregion 268 | 269 | #region IEnumerable 270 | public IEnumerator GetEnumerator () {return values.GetEnumerator ();} 271 | #endregion 272 | 273 | #region IList 274 | int IList.Add (object value) {return (values as IList).Add (value);} 275 | bool IList.Contains (object value) {return (values as IList).Contains (value);} 276 | int IList.IndexOf (object value) {return (values as IList).IndexOf (value);} 277 | void IList.Insert (int index, object value) {(values as IList).Insert (index, value);} 278 | void IList.Remove (object value) {(values as IList).Remove (value);} 279 | void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);} 280 | bool IList.IsFixedSize {get {return false;}} 281 | object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}} 282 | #endregion 283 | 284 | #region IList 285 | public int IndexOf (string item) {return values.IndexOf (item);} 286 | public void Insert (int index, string item) {values.Insert (index, item);} 287 | public void RemoveAt (int index) {values.RemoveAt (index);} 288 | 289 | private void AssertValid (int index) 290 | { 291 | if (c.Option == null) 292 | throw new InvalidOperationException ("OptionContext.Option is null."); 293 | if (index >= c.Option.MaxValueCount) 294 | throw new ArgumentOutOfRangeException (nameof(index)); 295 | if (c.Option.OptionValueType == OptionValueType.Required && 296 | index >= values.Count) 297 | throw new OptionException (string.Format ( 298 | c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName), 299 | c.OptionName); 300 | } 301 | 302 | public string this [int index] { 303 | get { 304 | AssertValid (index); 305 | return index >= values.Count ? null : values [index]; 306 | } 307 | set { 308 | values [index] = value; 309 | } 310 | } 311 | #endregion 312 | 313 | public List ToList () 314 | { 315 | return new List (values); 316 | } 317 | 318 | public string[] ToArray () 319 | { 320 | return values.ToArray (); 321 | } 322 | 323 | public override string ToString () 324 | { 325 | return string.Join (", ", values.ToArray ()); 326 | } 327 | } 328 | 329 | class OptionContext { 330 | private Option option; 331 | private string name; 332 | private int index; 333 | private OptionSet set; 334 | private OptionValueCollection c; 335 | 336 | public OptionContext (OptionSet set) 337 | { 338 | this.set = set; 339 | this.c = new OptionValueCollection (this); 340 | } 341 | 342 | public Option Option { 343 | get {return option;} 344 | set {option = value;} 345 | } 346 | 347 | public string OptionName { 348 | get {return name;} 349 | set {name = value;} 350 | } 351 | 352 | public int OptionIndex { 353 | get {return index;} 354 | set {index = value;} 355 | } 356 | 357 | public OptionSet OptionSet { 358 | get {return set;} 359 | } 360 | 361 | public OptionValueCollection OptionValues { 362 | get {return c;} 363 | } 364 | } 365 | 366 | enum OptionValueType { 367 | None, 368 | Optional, 369 | Required, 370 | } 371 | 372 | abstract class Option { 373 | string prototype, description; 374 | string[] names; 375 | OptionValueType type; 376 | int count; 377 | string[] separators; 378 | bool hidden; 379 | 380 | protected Option (string prototype, string description) 381 | : this (prototype, description, 1, false) 382 | { 383 | } 384 | 385 | protected Option (string prototype, string description, int maxValueCount) 386 | : this (prototype, description, maxValueCount, false) 387 | { 388 | } 389 | 390 | protected Option (string prototype, string description, int maxValueCount, bool hidden) 391 | { 392 | if (prototype == null) 393 | throw new ArgumentNullException (nameof(prototype)); 394 | if (prototype.Length == 0) 395 | throw new ArgumentException ("Cannot be the empty string.", nameof(prototype)); 396 | if (maxValueCount < 0) 397 | throw new ArgumentOutOfRangeException (nameof(maxValueCount)); 398 | 399 | this.prototype = prototype; 400 | this.description = description; 401 | this.count = maxValueCount; 402 | this.names = (this is OptionSet.Category) 403 | // append GetHashCode() so that "duplicate" categories have distinct 404 | // names, e.g. adding multiple "" categories should be valid. 405 | ? new[]{prototype + this.GetHashCode ()} 406 | : prototype.Split ('|'); 407 | 408 | if (this is OptionSet.Category) 409 | return; 410 | 411 | this.type = ParsePrototype (); 412 | this.hidden = hidden; 413 | 414 | if (this.count == 0 && type != OptionValueType.None) 415 | throw new ArgumentException ( 416 | "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + 417 | "OptionValueType.Optional.", 418 | nameof(maxValueCount)); 419 | if (this.type == OptionValueType.None && maxValueCount > 1) 420 | throw new ArgumentException ( 421 | string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), 422 | nameof(maxValueCount)); 423 | if (Array.IndexOf (names, "<>") >= 0 && 424 | ((names.Length == 1 && this.type != OptionValueType.None) || 425 | (names.Length > 1 && this.MaxValueCount > 1))) 426 | throw new ArgumentException ( 427 | "The default option handler '<>' cannot require values.", 428 | nameof(prototype)); 429 | } 430 | 431 | public string Prototype {get {return prototype;}} 432 | public string Description {get {return description;}} 433 | public OptionValueType OptionValueType {get {return type;}} 434 | public int MaxValueCount {get {return count;}} 435 | public bool Hidden {get {return hidden;}} 436 | 437 | public string[] GetNames () 438 | { 439 | return (string[]) names.Clone (); 440 | } 441 | 442 | public string[] GetValueSeparators () 443 | { 444 | if (separators == null) 445 | return new string [0]; 446 | return (string[]) separators.Clone (); 447 | } 448 | 449 | protected static T Parse (string value, OptionContext c) 450 | { 451 | var tt = typeof (T).GetTypeInfo(); 452 | bool nullable = tt.IsValueType && tt.IsGenericType && 453 | !tt.IsGenericTypeDefinition && 454 | tt.GetGenericTypeDefinition () == typeof (Nullable<>); 455 | Type targetType = nullable ? tt.GetGenericArguments () [0] : typeof (T); 456 | T t = default (T); 457 | try { 458 | if (value != null) 459 | t = (T) Convert.ChangeType(value, targetType); 460 | } 461 | catch (Exception e) { 462 | throw new OptionException ( 463 | string.Format ( 464 | c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."), 465 | value, targetType.Name, c.OptionName), 466 | c.OptionName, e); 467 | } 468 | return t; 469 | } 470 | 471 | internal string[] Names {get {return names;}} 472 | internal string[] ValueSeparators {get {return separators;}} 473 | 474 | static readonly char[] NameTerminator = new char[]{'=', ':'}; 475 | 476 | private OptionValueType ParsePrototype () 477 | { 478 | char type = '\0'; 479 | List seps = new List (); 480 | for (int i = 0; i < names.Length; ++i) { 481 | string name = names [i]; 482 | if (name.Length == 0) 483 | throw new ArgumentException ("Empty option names are not supported.", "prototype"); 484 | 485 | int end = name.IndexOfAny (NameTerminator); 486 | if (end == -1) 487 | continue; 488 | names [i] = name.Substring (0, end); 489 | if (type == '\0' || type == name [end]) 490 | type = name [end]; 491 | else 492 | throw new ArgumentException ( 493 | string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), 494 | "prototype"); 495 | AddSeparators (name, end, seps); 496 | } 497 | 498 | if (type == '\0') 499 | return OptionValueType.None; 500 | 501 | if (count <= 1 && seps.Count != 0) 502 | throw new ArgumentException ( 503 | string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count), 504 | "prototype"); 505 | if (count > 1) { 506 | if (seps.Count == 0) 507 | this.separators = new string[]{":", "="}; 508 | else if (seps.Count == 1 && seps [0].Length == 0) 509 | this.separators = null; 510 | else 511 | this.separators = seps.ToArray (); 512 | } 513 | 514 | return type == '=' ? OptionValueType.Required : OptionValueType.Optional; 515 | } 516 | 517 | private static void AddSeparators (string name, int end, ICollection seps) 518 | { 519 | int start = -1; 520 | for (int i = end+1; i < name.Length; ++i) { 521 | switch (name [i]) { 522 | case '{': 523 | if (start != -1) 524 | throw new ArgumentException ( 525 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name), 526 | "prototype"); 527 | start = i+1; 528 | break; 529 | case '}': 530 | if (start == -1) 531 | throw new ArgumentException ( 532 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name), 533 | "prototype"); 534 | seps.Add (name.Substring (start, i-start)); 535 | start = -1; 536 | break; 537 | default: 538 | if (start == -1) 539 | seps.Add (name [i].ToString ()); 540 | break; 541 | } 542 | } 543 | if (start != -1) 544 | throw new ArgumentException ( 545 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name), 546 | "prototype"); 547 | } 548 | 549 | public void Invoke (OptionContext c) 550 | { 551 | OnParseComplete (c); 552 | c.OptionName = null; 553 | c.Option = null; 554 | c.OptionValues.Clear (); 555 | } 556 | 557 | protected abstract void OnParseComplete (OptionContext c); 558 | 559 | public override string ToString () 560 | { 561 | return Prototype; 562 | } 563 | } 564 | 565 | abstract class ArgumentSource { 566 | 567 | protected ArgumentSource () 568 | { 569 | } 570 | 571 | public abstract string[] GetNames (); 572 | public abstract string Description { get; } 573 | public abstract bool GetArguments (string value, out IEnumerable replacement); 574 | 575 | public static IEnumerable GetArgumentsFromFile (string file) 576 | { 577 | return GetArguments (File.OpenText (file), true); 578 | } 579 | 580 | public static IEnumerable GetArguments (TextReader reader) 581 | { 582 | return GetArguments (reader, false); 583 | } 584 | 585 | // Cribbed from mcs/driver.cs:LoadArgs(string) 586 | static IEnumerable GetArguments (TextReader reader, bool close) 587 | { 588 | try { 589 | StringBuilder arg = new StringBuilder (); 590 | 591 | string line; 592 | while ((line = reader.ReadLine ()) != null) { 593 | int t = line.Length; 594 | 595 | for (int i = 0; i < t; i++) { 596 | char c = line [i]; 597 | 598 | if (c == '"' || c == '\'') { 599 | char end = c; 600 | 601 | for (i++; i < t; i++){ 602 | c = line [i]; 603 | 604 | if (c == end) 605 | break; 606 | arg.Append (c); 607 | } 608 | } else if (c == ' ') { 609 | if (arg.Length > 0) { 610 | yield return arg.ToString (); 611 | arg.Length = 0; 612 | } 613 | } else 614 | arg.Append (c); 615 | } 616 | if (arg.Length > 0) { 617 | yield return arg.ToString (); 618 | arg.Length = 0; 619 | } 620 | } 621 | } 622 | finally { 623 | if (close) 624 | reader.Dispose(); 625 | } 626 | } 627 | } 628 | 629 | class ResponseFileSource : ArgumentSource { 630 | 631 | public override string[] GetNames () 632 | { 633 | return new string[]{"@file"}; 634 | } 635 | 636 | public override string Description { 637 | get {return "Read response file for more options.";} 638 | } 639 | 640 | public override bool GetArguments (string value, out IEnumerable replacement) 641 | { 642 | if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) { 643 | replacement = null; 644 | return false; 645 | } 646 | replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1)); 647 | return true; 648 | } 649 | } 650 | 651 | class OptionException : Exception { 652 | private string option; 653 | 654 | public OptionException () 655 | { 656 | } 657 | 658 | public OptionException (string message, string optionName) 659 | : base (message) 660 | { 661 | this.option = optionName; 662 | } 663 | 664 | public OptionException (string message, string optionName, Exception innerException) 665 | : base (message, innerException) 666 | { 667 | this.option = optionName; 668 | } 669 | 670 | public string OptionName { 671 | get {return this.option;} 672 | } 673 | } 674 | 675 | delegate void OptionAction (TKey key, TValue value); 676 | 677 | class OptionSet : KeyedCollection 678 | { 679 | public OptionSet () 680 | : this (delegate (string f) {return f;}) 681 | { 682 | } 683 | 684 | public OptionSet (Converter localizer) 685 | { 686 | this.localizer = localizer; 687 | this.roSources = new ReadOnlyCollection(sources); 688 | } 689 | 690 | Converter localizer; 691 | 692 | public Converter MessageLocalizer { 693 | get {return localizer;} 694 | } 695 | 696 | List sources = new List (); 697 | ReadOnlyCollection roSources; 698 | 699 | public ReadOnlyCollection ArgumentSources { 700 | get {return roSources;} 701 | } 702 | 703 | 704 | protected override string GetKeyForItem (Option item) 705 | { 706 | if (item == null) 707 | throw new ArgumentNullException ("option"); 708 | if (item.Names != null && item.Names.Length > 0) 709 | return item.Names [0]; 710 | // This should never happen, as it's invalid for Option to be 711 | // constructed w/o any names. 712 | throw new InvalidOperationException ("Option has no names!"); 713 | } 714 | 715 | [Obsolete ("Use KeyedCollection.this[string]")] 716 | protected Option GetOptionForName (string option) 717 | { 718 | if (option == null) 719 | throw new ArgumentNullException (nameof(option)); 720 | try { 721 | return base [option]; 722 | } 723 | catch (KeyNotFoundException) { 724 | return null; 725 | } 726 | } 727 | 728 | protected override void InsertItem (int index, Option item) 729 | { 730 | base.InsertItem (index, item); 731 | AddImpl (item); 732 | } 733 | 734 | protected override void RemoveItem (int index) 735 | { 736 | Option p = Items [index]; 737 | base.RemoveItem (index); 738 | // KeyedCollection.RemoveItem() handles the 0th item 739 | for (int i = 1; i < p.Names.Length; ++i) { 740 | Dictionary.Remove (p.Names [i]); 741 | } 742 | } 743 | 744 | protected override void SetItem (int index, Option item) 745 | { 746 | base.SetItem (index, item); 747 | AddImpl (item); 748 | } 749 | 750 | private void AddImpl (Option option) 751 | { 752 | if (option == null) 753 | throw new ArgumentNullException (nameof(option)); 754 | List added = new List (option.Names.Length); 755 | try { 756 | // KeyedCollection.InsertItem/SetItem handle the 0th name. 757 | for (int i = 1; i < option.Names.Length; ++i) { 758 | Dictionary.Add (option.Names [i], option); 759 | added.Add (option.Names [i]); 760 | } 761 | } 762 | catch (Exception) { 763 | foreach (string name in added) 764 | Dictionary.Remove (name); 765 | throw; 766 | } 767 | } 768 | 769 | public OptionSet Add (string header) 770 | { 771 | if (header == null) 772 | throw new ArgumentNullException (nameof(header)); 773 | Add (new Category (header)); 774 | return this; 775 | } 776 | 777 | internal sealed class Category : Option { 778 | 779 | // Prototype starts with '=' because this is an invalid prototype 780 | // (see Option.ParsePrototype(), and thus it'll prevent Category 781 | // instances from being accidentally used as normal options. 782 | public Category (string description) 783 | : base ("=:Category:= " + description, description) 784 | { 785 | } 786 | 787 | protected override void OnParseComplete (OptionContext c) 788 | { 789 | throw new NotSupportedException ("Category.OnParseComplete should not be invoked."); 790 | } 791 | } 792 | 793 | 794 | public new OptionSet Add (Option option) 795 | { 796 | base.Add (option); 797 | return this; 798 | } 799 | 800 | sealed class ActionOption : Option { 801 | Action action; 802 | 803 | public ActionOption (string prototype, string description, int count, Action action) 804 | : this (prototype, description, count, action, false) 805 | { 806 | } 807 | 808 | public ActionOption (string prototype, string description, int count, Action action, bool hidden) 809 | : base (prototype, description, count, hidden) 810 | { 811 | this.action = action ?? throw new ArgumentNullException (nameof(action)); 812 | } 813 | 814 | protected override void OnParseComplete (OptionContext c) 815 | { 816 | action (c.OptionValues); 817 | } 818 | } 819 | 820 | public OptionSet Add (string prototype, Action action) 821 | { 822 | return Add (prototype, null, action); 823 | } 824 | 825 | public OptionSet Add (string prototype, string description, Action action) 826 | { 827 | return Add (prototype, description, action, false); 828 | } 829 | 830 | public OptionSet Add (string prototype, string description, Action action, bool hidden) 831 | { 832 | if (action == null) 833 | throw new ArgumentNullException (nameof(action)); 834 | Option p = new ActionOption (prototype, description, 1, 835 | delegate (OptionValueCollection v) 836 | { 837 | var v0 = v[0]; 838 | if (!string.IsNullOrWhiteSpace(v0)) 839 | { 840 | action(v0); 841 | } 842 | }, hidden); 843 | base.Add (p); 844 | return this; 845 | } 846 | 847 | public OptionSet Add (string prototype, OptionAction action) 848 | { 849 | return Add (prototype, null, action); 850 | } 851 | 852 | public OptionSet Add (string prototype, string description, OptionAction action) 853 | { 854 | return Add (prototype, description, action, false); 855 | } 856 | 857 | public OptionSet Add (string prototype, string description, OptionAction action, bool hidden) { 858 | if (action == null) 859 | throw new ArgumentNullException (nameof(action)); 860 | Option p = new ActionOption (prototype, description, 2, 861 | delegate (OptionValueCollection v) {action (v [0], v [1]);}, hidden); 862 | base.Add (p); 863 | return this; 864 | } 865 | 866 | sealed class ActionOption : Option { 867 | Action action; 868 | 869 | public ActionOption (string prototype, string description, Action action) 870 | : base (prototype, description, 1) 871 | { 872 | this.action = action ?? throw new ArgumentNullException (nameof(action)); 873 | } 874 | 875 | protected override void OnParseComplete (OptionContext c) 876 | { 877 | action (Parse (c.OptionValues [0], c)); 878 | } 879 | } 880 | 881 | sealed class ActionOption : Option { 882 | OptionAction action; 883 | 884 | public ActionOption (string prototype, string description, OptionAction action) 885 | : base (prototype, description, 2) 886 | { 887 | this.action = action ?? throw new ArgumentNullException (nameof(action)); 888 | } 889 | 890 | protected override void OnParseComplete (OptionContext c) 891 | { 892 | action ( 893 | Parse (c.OptionValues [0], c), 894 | Parse (c.OptionValues [1], c)); 895 | } 896 | } 897 | 898 | public OptionSet Add (string prototype, Action action) 899 | { 900 | return Add (prototype, null, action); 901 | } 902 | 903 | public OptionSet Add (string prototype, string description, Action action) 904 | { 905 | return Add (new ActionOption (prototype, description, action)); 906 | } 907 | 908 | public OptionSet Add (string prototype, OptionAction action) 909 | { 910 | return Add (prototype, null, action); 911 | } 912 | 913 | public OptionSet Add (string prototype, string description, OptionAction action) 914 | { 915 | return Add (new ActionOption (prototype, description, action)); 916 | } 917 | 918 | public OptionSet Add (ArgumentSource source) 919 | { 920 | if (source == null) 921 | throw new ArgumentNullException (nameof(source)); 922 | sources.Add (source); 923 | return this; 924 | } 925 | 926 | protected virtual OptionContext CreateOptionContext () 927 | { 928 | return new OptionContext (this); 929 | } 930 | 931 | public List Parse (IEnumerable arguments) 932 | { 933 | if (arguments == null) 934 | throw new ArgumentNullException (nameof(arguments)); 935 | OptionContext c = CreateOptionContext (); 936 | c.OptionIndex = -1; 937 | bool process = true; 938 | List unprocessed = new List (); 939 | Option def = Contains ("<>") ? this ["<>"] : null; 940 | ArgumentEnumerator ae = new ArgumentEnumerator (arguments); 941 | foreach (string argument in ae) { 942 | ++c.OptionIndex; 943 | if (argument == "--") { 944 | process = false; 945 | continue; 946 | } 947 | if (!process) { 948 | Unprocessed (unprocessed, def, c, argument); 949 | continue; 950 | } 951 | if (AddSource (ae, argument)) 952 | continue; 953 | if (!Parse (argument, c)) 954 | Unprocessed (unprocessed, def, c, argument); 955 | } 956 | if (c.Option != null) 957 | c.Option.Invoke (c); 958 | return unprocessed; 959 | } 960 | 961 | class ArgumentEnumerator : IEnumerable { 962 | List> sources = new List> (); 963 | 964 | public ArgumentEnumerator (IEnumerable arguments) 965 | { 966 | sources.Add (arguments.GetEnumerator ()); 967 | } 968 | 969 | public void Add (IEnumerable arguments) 970 | { 971 | sources.Add (arguments.GetEnumerator ()); 972 | } 973 | 974 | public IEnumerator GetEnumerator () 975 | { 976 | do { 977 | IEnumerator c = sources [sources.Count-1]; 978 | if (c.MoveNext ()) 979 | yield return c.Current; 980 | else { 981 | c.Dispose (); 982 | sources.RemoveAt (sources.Count-1); 983 | } 984 | } while (sources.Count > 0); 985 | } 986 | 987 | IEnumerator IEnumerable.GetEnumerator () 988 | { 989 | return GetEnumerator (); 990 | } 991 | } 992 | 993 | bool AddSource (ArgumentEnumerator ae, string argument) 994 | { 995 | foreach (ArgumentSource source in sources) { 996 | IEnumerable replacement; 997 | if (!source.GetArguments (argument, out replacement)) 998 | continue; 999 | ae.Add (replacement); 1000 | return true; 1001 | } 1002 | return false; 1003 | } 1004 | 1005 | private static bool Unprocessed (ICollection extra, Option def, OptionContext c, string argument) 1006 | { 1007 | if (def == null) { 1008 | extra.Add (argument); 1009 | return false; 1010 | } 1011 | c.OptionValues.Add (argument); 1012 | c.Option = def; 1013 | c.Option.Invoke (c); 1014 | return false; 1015 | } 1016 | 1017 | private readonly Regex ValueOption = new Regex ( 1018 | @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); 1019 | 1020 | protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) 1021 | { 1022 | if (argument == null) 1023 | throw new ArgumentNullException (nameof(argument)); 1024 | 1025 | flag = name = sep = value = null; 1026 | Match m = ValueOption.Match (argument); 1027 | if (!m.Success) { 1028 | return false; 1029 | } 1030 | flag = m.Groups ["flag"].Value; 1031 | name = m.Groups ["name"].Value; 1032 | if (m.Groups ["sep"].Success && m.Groups ["value"].Success) { 1033 | sep = m.Groups ["sep"].Value; 1034 | value = m.Groups ["value"].Value; 1035 | } 1036 | return true; 1037 | } 1038 | 1039 | protected virtual bool Parse (string argument, OptionContext c) 1040 | { 1041 | if (c.Option != null) { 1042 | ParseValue (argument, c); 1043 | return true; 1044 | } 1045 | 1046 | string f, n, s, v; 1047 | if (!GetOptionParts (argument, out f, out n, out s, out v)) 1048 | return false; 1049 | 1050 | Option p; 1051 | if (Contains (n)) { 1052 | p = this [n]; 1053 | c.OptionName = f + n; 1054 | c.Option = p; 1055 | switch (p.OptionValueType) { 1056 | case OptionValueType.None: 1057 | c.OptionValues.Add (n); 1058 | c.Option.Invoke (c); 1059 | break; 1060 | case OptionValueType.Optional: 1061 | case OptionValueType.Required: 1062 | ParseValue (v, c); 1063 | break; 1064 | } 1065 | return true; 1066 | } 1067 | // no match; is it a bool option? 1068 | if (ParseBool (argument, n, c)) 1069 | return true; 1070 | // is it a bundled option? 1071 | if (ParseBundledValue (f, string.Concat (n + s + v), c)) 1072 | return true; 1073 | 1074 | return false; 1075 | } 1076 | 1077 | private void ParseValue (string option, OptionContext c) 1078 | { 1079 | if (option != null) 1080 | foreach (string o in c.Option.ValueSeparators != null 1081 | ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None) 1082 | : new string[]{option}) { 1083 | c.OptionValues.Add (o); 1084 | } 1085 | if (c.OptionValues.Count == c.Option.MaxValueCount || 1086 | c.Option.OptionValueType == OptionValueType.Optional) 1087 | c.Option.Invoke (c); 1088 | else if (c.OptionValues.Count > c.Option.MaxValueCount) { 1089 | throw new OptionException (localizer (string.Format ( 1090 | "Error: Found {0} option values when expecting {1}.", 1091 | c.OptionValues.Count, c.Option.MaxValueCount)), 1092 | c.OptionName); 1093 | } 1094 | } 1095 | 1096 | private bool ParseBool (string option, string n, OptionContext c) 1097 | { 1098 | Option p; 1099 | string rn; 1100 | if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') && 1101 | Contains ((rn = n.Substring (0, n.Length-1)))) { 1102 | p = this [rn]; 1103 | string v = n [n.Length-1] == '+' ? option : null; 1104 | c.OptionName = option; 1105 | c.Option = p; 1106 | c.OptionValues.Add (v); 1107 | p.Invoke (c); 1108 | return true; 1109 | } 1110 | return false; 1111 | } 1112 | 1113 | private bool ParseBundledValue (string f, string n, OptionContext c) 1114 | { 1115 | if (f != "-") 1116 | return false; 1117 | for (int i = 0; i < n.Length; ++i) { 1118 | Option p; 1119 | string opt = f + n [i].ToString (); 1120 | string rn = n [i].ToString (); 1121 | if (!Contains (rn)) { 1122 | if (i == 0) 1123 | return false; 1124 | throw new OptionException (string.Format (localizer ( 1125 | "Cannot bundle unregistered option '{0}'."), opt), opt); 1126 | } 1127 | p = this [rn]; 1128 | switch (p.OptionValueType) { 1129 | case OptionValueType.None: 1130 | Invoke (c, opt, n, p); 1131 | break; 1132 | case OptionValueType.Optional: 1133 | case OptionValueType.Required: { 1134 | string v = n.Substring (i+1); 1135 | c.Option = p; 1136 | c.OptionName = opt; 1137 | ParseValue (v.Length != 0 ? v : null, c); 1138 | return true; 1139 | } 1140 | default: 1141 | throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType); 1142 | } 1143 | } 1144 | return true; 1145 | } 1146 | 1147 | private static void Invoke (OptionContext c, string name, string value, Option option) 1148 | { 1149 | c.OptionName = name; 1150 | c.Option = option; 1151 | c.OptionValues.Add (value); 1152 | option.Invoke (c); 1153 | } 1154 | 1155 | private const int OptionWidth = 29; 1156 | private const int Description_FirstWidth = 80 - OptionWidth; 1157 | private const int Description_RemWidth = 80 - OptionWidth - 2; 1158 | 1159 | public void WriteOptionDescriptions (TextWriter o) 1160 | { 1161 | foreach (Option p in this) { 1162 | int written = 0; 1163 | 1164 | if (p.Hidden) 1165 | continue; 1166 | 1167 | Category c = p as Category; 1168 | if (c != null) { 1169 | WriteDescription (o, p.Description, "", 80, 80); 1170 | continue; 1171 | } 1172 | 1173 | if (!WriteOptionPrototype (o, p, ref written)) 1174 | continue; 1175 | 1176 | if (written < OptionWidth) 1177 | o.Write (new string (' ', OptionWidth - written)); 1178 | else { 1179 | o.WriteLine (); 1180 | o.Write (new string (' ', OptionWidth)); 1181 | } 1182 | 1183 | WriteDescription (o, p.Description, new string (' ', OptionWidth+2), 1184 | Description_FirstWidth -1, Description_RemWidth - 2); 1185 | } 1186 | 1187 | foreach (ArgumentSource s in sources) { 1188 | string[] names = s.GetNames (); 1189 | if (names == null || names.Length == 0) 1190 | continue; 1191 | 1192 | int written = 0; 1193 | 1194 | Write (o, ref written, " "); 1195 | Write (o, ref written, names [0]); 1196 | for (int i = 1; i < names.Length; ++i) { 1197 | Write (o, ref written, ", "); 1198 | Write (o, ref written, names [i]); 1199 | } 1200 | 1201 | if (written < OptionWidth) 1202 | o.Write (new string (' ', OptionWidth - written)); 1203 | else { 1204 | o.WriteLine (); 1205 | o.Write (new string (' ', OptionWidth)); 1206 | } 1207 | 1208 | WriteDescription (o, s.Description, new string (' ', OptionWidth+2), 1209 | Description_FirstWidth, Description_RemWidth); 1210 | } 1211 | } 1212 | 1213 | void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth) 1214 | { 1215 | bool indent = false; 1216 | foreach (string line in GetLines (localizer (GetDescription (value)), firstWidth, remWidth)) { 1217 | if (indent) 1218 | o.Write (prefix); 1219 | o.WriteLine (line); 1220 | indent = true; 1221 | } 1222 | } 1223 | 1224 | bool WriteOptionPrototype (TextWriter o, Option p, ref int written) 1225 | { 1226 | string[] names = p.Names; 1227 | 1228 | int i = GetNextOptionIndex (names, 0); 1229 | if (i == names.Length) 1230 | return false; 1231 | 1232 | if (names [i].Length == 1) { 1233 | Write (o, ref written, " -"); 1234 | Write (o, ref written, names [0]); 1235 | } 1236 | else { 1237 | Write (o, ref written, " --"); 1238 | Write (o, ref written, names [0]); 1239 | } 1240 | 1241 | for ( i = GetNextOptionIndex (names, i+1); 1242 | i < names.Length; i = GetNextOptionIndex (names, i+1)) { 1243 | Write (o, ref written, ", "); 1244 | Write (o, ref written, names [i].Length == 1 ? "-" : "--"); 1245 | Write (o, ref written, names [i]); 1246 | } 1247 | 1248 | if (p.OptionValueType == OptionValueType.Optional || 1249 | p.OptionValueType == OptionValueType.Required) { 1250 | if (p.OptionValueType == OptionValueType.Optional) { 1251 | Write (o, ref written, localizer ("[")); 1252 | } 1253 | Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description))); 1254 | string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 1255 | ? p.ValueSeparators [0] 1256 | : " "; 1257 | for (int c = 1; c < p.MaxValueCount; ++c) { 1258 | Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description))); 1259 | } 1260 | if (p.OptionValueType == OptionValueType.Optional) { 1261 | Write (o, ref written, localizer ("]")); 1262 | } 1263 | } 1264 | return true; 1265 | } 1266 | 1267 | static int GetNextOptionIndex (string[] names, int i) 1268 | { 1269 | while (i < names.Length && names [i] == "<>") { 1270 | ++i; 1271 | } 1272 | return i; 1273 | } 1274 | 1275 | static void Write (TextWriter o, ref int n, string s) 1276 | { 1277 | n += s.Length; 1278 | o.Write (s); 1279 | } 1280 | 1281 | private static string GetArgumentName (int index, int maxIndex, string description) 1282 | { 1283 | if (description == null) 1284 | return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); 1285 | string[] nameStart; 1286 | if (maxIndex == 1) 1287 | nameStart = new string[]{"{0:", "{"}; 1288 | else 1289 | nameStart = new string[]{"{" + index + ":"}; 1290 | for (int i = 0; i < nameStart.Length; ++i) { 1291 | int start, j = 0; 1292 | do { 1293 | start = description.IndexOf (nameStart [i], j); 1294 | } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false); 1295 | if (start == -1) 1296 | continue; 1297 | int end = description.IndexOf ("}", start); 1298 | if (end == -1) 1299 | continue; 1300 | return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length); 1301 | } 1302 | return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); 1303 | } 1304 | 1305 | private static string GetDescription (string description) 1306 | { 1307 | if (description == null) 1308 | return string.Empty; 1309 | StringBuilder sb = new StringBuilder (description.Length); 1310 | int start = -1; 1311 | for (int i = 0; i < description.Length; ++i) { 1312 | switch (description [i]) { 1313 | case '{': 1314 | if (i == start) { 1315 | sb.Append ('{'); 1316 | start = -1; 1317 | } 1318 | else if (start < 0) 1319 | start = i + 1; 1320 | break; 1321 | case '}': 1322 | if (start < 0) { 1323 | if ((i+1) == description.Length || description [i+1] != '}') 1324 | throw new InvalidOperationException ("Invalid option description: " + description); 1325 | ++i; 1326 | sb.Append ("}"); 1327 | } 1328 | else { 1329 | sb.Append (description.Substring (start, i - start)); 1330 | start = -1; 1331 | } 1332 | break; 1333 | case ':': 1334 | if (start < 0) 1335 | goto default; 1336 | start = i + 1; 1337 | break; 1338 | default: 1339 | if (start < 0) 1340 | sb.Append (description [i]); 1341 | break; 1342 | } 1343 | } 1344 | return sb.ToString (); 1345 | } 1346 | 1347 | private static IEnumerable GetLines (string description, int firstWidth, int remWidth) 1348 | { 1349 | return StringCoda.WrappedLines (description, firstWidth, remWidth); 1350 | } 1351 | } 1352 | } 1353 | 1354 | -------------------------------------------------------------------------------- /asset/PiggyLicense.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff0\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi0\deflang3081\deflangfe3081\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} 2 | {\f37\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} 3 | {\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;} 4 | {\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} 5 | {\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;} 6 | {\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f42\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f43\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} 7 | {\f45\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f46\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f47\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f48\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} 8 | {\f49\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f50\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f42\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f43\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} 9 | {\f45\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f46\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f47\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f48\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} 10 | {\f49\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f50\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f412\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\f413\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;} 11 | {\f415\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f416\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\f417\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\f418\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);} 12 | {\f419\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\f420\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} 13 | {\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} 14 | {\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} 15 | {\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} 16 | {\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} 17 | {\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} 18 | {\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;} 19 | {\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);} 20 | {\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} 21 | {\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} 22 | {\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} 23 | {\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} 24 | {\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} 25 | {\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} 26 | {\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} 27 | {\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} 28 | {\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;} 29 | {\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;} 30 | {\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;} 31 | {\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} 32 | {\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} 33 | {\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}} 34 | {\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0; 35 | \red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\chyperlink\ctint255\cshade255\red5\green99\blue193;}{\*\defchp \fs22\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap 36 | \ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 37 | \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* 38 | \ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1 39 | \widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang3081\langfe3081\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp3081\langfenp3081 \snext11 \ssemihidden \sunhideused 40 | Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \ul\cf17 \sbasedon10 \sunhideused \styrsid9926404 Hyperlink;}}{\*\listtable{\list\listtemplateid-442828566\listsimple{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0 41 | \levelstartat1\levelold\levelspace0\levelindent0{\leveltext\'02\'00);}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \f37\fbias0\hres0\chhres0 }{\listname ;}\listid1082141537}{\list\listtemplateid-1279628680\listsimple{\listlevel\levelnfc2\levelnfcn2 42 | \leveljc0\leveljcn0\levelfollow0\levelstartat1\levelold\levelspace0\levelindent0{\leveltext\'02\'00);}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \f37\fbias0\hres0\chhres0 }{\listname ;}\listid1363284744}{\list\listtemplateid-442828566\listsimple 43 | {\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelold\levelspace0\levelindent0{\leveltext\'02\'00);}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \f37\fbias0\hres0\chhres0 }{\listname ;}\listid2092311831}} 44 | {\*\listoverridetable{\listoverride\listid1082141537\listoverridecount0\ls1}{\listoverride\listid1363284744\listoverridecount0\ls2}{\listoverride\listid2092311831\listoverridecount0\ls3}}{\*\rsidtbl \rsid1586863\rsid2361176\rsid8917066\rsid9926404 45 | \rsid12277323\rsid12720317\rsid15671506\rsid16323729}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author Nicholas Blumhardt}{\operator Nicholas Blumhardt} 46 | {\creatim\yr2016\mo6\dy16\hr14\min25}{\revtim\yr2017\mo7\dy19\hr16\min22}{\version4}{\edmins2}{\nofpages7}{\nofwords1532}{\nofchars8737}{\nofcharsws10249}{\vern37}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}} 47 | \paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect 48 | \widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120\dghorigin1701 49 | \dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale100\rsidroot12720317 \nouicompat \fet0{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1 50 | \pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5 51 | \pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang 52 | {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\ql \li0\ri0\sa200\sl276\slmult1\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 53 | \fs22\lang1033\langfe1033\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \ab\af37\afs30 \ltrch\fcs0 \b\f37\fs30\lang9\langfe1033\langnp9\insrsid2361176 \hich\af37\dbch\af31505\loch\f37 Piggy}{\rtlch\fcs1 \ab\af37\afs30 54 | \ltrch\fcs0 \b\f37\fs30\lang9\langfe1033\langnp9\insrsid12277323 \hich\af37\dbch\af31505\loch\f37 License Agreement 55 | \par }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid2361176 \hich\af37\dbch\af31505\loch\f37 Piggy}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 56 | \hich\af37\dbch\af31505\loch\f37 c}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid12277323 \hich\af37\dbch\af31505\loch\f37 \hich\f37 opyright \'a9\loch\f37 201}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 57 | \i\f37\fs20\lang9\langfe1033\langnp9\insrsid2361176 \hich\af37\dbch\af31505\loch\f37 7}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid12277323 \hich\af37\dbch\af31505\loch\f37 }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 58 | \i\f37\fs20\lang9\langfe1033\langnp9\insrsid15671506 \hich\af37\dbch\af31505\loch\f37 Datalust }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid12277323 \hich\af37\dbch\af31505\loch\f37 Pty Ltd}{\rtlch\fcs1 59 | \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 \hich\af37\dbch\af31505\loch\f37 and contributors}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid12277323 60 | \hich\af37\dbch\af31505\loch\f37 }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 \loch\af37\dbch\af31505\hich\f37 \endash }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 61 | \i\f37\fs20\lang9\langfe1033\langnp9\insrsid12277323 \hich\af37\dbch\af31505\loch\f37 }{\field{\*\fldinst {\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 \hich\af37\dbch\af31505\loch\f37 62 | HYPERLINK "https://datalust.co" }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid8917066 {\*\datafield 63 | 00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b42000000680074007400700073003a002f002f0064006100740061006c007500730074002e0063006f002f000000795881f43b1d7f48af2c825dc485276300000000a5ab000300}}}{\fldrslt {\rtlch\fcs1 64 | \ai\af37\afs20 \ltrch\fcs0 \cs15\i\f37\fs20\ul\cf17\lang9\langfe1033\langnp9\insrsid9926404\charrsid16323729 \hich\af37\dbch\af31505\loch\f37 https://datalust.co}}}\sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 65 | \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 66 | \par \hich\af37\dbch\af31505\loch\f37 Distributed under the Apache License,}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404\charrsid9926404 \hich\af37\dbch\af31505\loch\f37 Version 2.0, January 2004 \hich\f37 67 | \endash \loch\f37 }{\field{\*\fldinst {\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 \hich\af37\dbch\af31505\loch\f37 HYPERLINK "}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 68 | \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404\charrsid9926404 \hich\af37\dbch\af31505\loch\f37 http://www.apache.org/licenses/}{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 69 | \hich\af37\dbch\af31505\loch\f37 " }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid8917066 {\*\datafield 70 | 00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b5800000068007400740070003a002f002f007700770077002e006100700061006300680065002e006f00720067002f006c006900630065006e007300650073002f000000795881f43b1d7f48af2c825dc48527630000 71 | 0000a5ab000300}}}{\fldrslt {\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \cs15\i\f37\fs20\ul\cf17\lang9\langfe1033\langnp9\insrsid9926404\charrsid16323729 \hich\af37\dbch\af31505\loch\f37 http://www.apache.org/licenses/}}}\sectd \ltrsect 72 | \linex0\sectdefaultcl\sftnbj {\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \i\f37\fs20\lang9\langfe1033\langnp9\insrsid9926404 \hich\af37\dbch\af31505\loch\f37 }{\rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 73 | \i\f37\fs20\lang9\langfe1033\langnp9\insrsid12277323\charrsid9926404 74 | \par }\pard \ltrpar\ql \li0\ri0\sa200\sl276\slmult1\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0\pararsid9926404 {\rtlch\fcs1 \af37 \ltrch\fcs0 \f37\lang9\langfe1033\langnp9\insrsid9926404\charrsid9926404 75 | \par \hich\af37\dbch\af31505\loch\f37 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 76 | \par 77 | \par \hich\af37\dbch\af31505\loch\f37 1. Definitions. 78 | \par 79 | \par \hich\af37\dbch\af31505\loch\f37 "License" shall mean the term\hich\af37\dbch\af31505\loch\f37 s and conditions for use, reproduction, 80 | \par \hich\af37\dbch\af31505\loch\f37 and distribution as defined by Sections 1 through 9 of this document. 81 | \par 82 | \par \hich\af37\dbch\af31505\loch\f37 "Licensor" shall mean the copyright owner or entity authorized by 83 | \par \hich\af37\dbch\af31505\loch\f37 the copyright owner that is granting the License. 84 | \par 85 | \par \hich\af37\dbch\af31505\loch\f37 "Leg\hich\af37\dbch\af31505\loch\f37 al Entity" shall mean the union of the acting entity and all 86 | \par \hich\af37\dbch\af31505\loch\f37 other entities that control, are controlled by, or are under common 87 | \par \hich\af37\dbch\af31505\loch\f37 control with that entity. For the purposes of this definition, 88 | \par \hich\af37\dbch\af31505\loch\f37 "control" means (i) the power, direct or indir\hich\af37\dbch\af31505\loch\f37 ect, to cause the 89 | \par \hich\af37\dbch\af31505\loch\f37 direction or management of such entity, whether by contract or 90 | \par \hich\af37\dbch\af31505\loch\f37 otherwise, or (ii) ownership of fifty percent (50%) or more of the 91 | \par \hich\af37\dbch\af31505\loch\f37 outstanding shares, or (iii) beneficial ownership of such entity. 92 | \par 93 | \par \hich\af37\dbch\af31505\loch\f37 "You" (or "Your")\hich\af37\dbch\af31505\loch\f37 shall mean an individual or Legal Entity 94 | \par \hich\af37\dbch\af31505\loch\f37 exercising permissions granted by this License. 95 | \par 96 | \par \hich\af37\dbch\af31505\loch\f37 "Source" form shall mean the preferred form for making modifications, 97 | \par \hich\af37\dbch\af31505\loch\f37 including but not limited to software source code, documentation 98 | \par \hich\af37\dbch\af31505\loch\f37 source, and configuration files. 99 | \par 100 | \par \hich\af37\dbch\af31505\loch\f37 "Object" form shall mean any form resulting from mechanical 101 | \par \hich\af37\dbch\af31505\loch\f37 transformation or translation of a Source form, including but 102 | \par \hich\af37\dbch\af31505\loch\f37 not limited to compiled object code, generated documentation, 103 | \par \hich\af37\dbch\af31505\loch\f37 and conv\hich\af37\dbch\af31505\loch\f37 ersions to other media types. 104 | \par 105 | \par \hich\af37\dbch\af31505\loch\f37 "Work" shall mean the work of authorship, whether in Source or 106 | \par \hich\af37\dbch\af31505\loch\f37 Object form, made available under the License, as indicated by a 107 | \par \hich\af37\dbch\af31505\loch\f37 copyright notice that is included in or attached to the work 108 | \par \hich\af37\dbch\af31505\loch\f37 (an example \hich\af37\dbch\af31505\loch\f37 is provided in the Appendix below). 109 | \par 110 | \par \hich\af37\dbch\af31505\loch\f37 "Derivative Works" shall mean any work, whether in Source or Object 111 | \par \hich\af37\dbch\af31505\loch\f37 form, that is based on (or derived from) the Work and for which the 112 | \par \hich\af37\dbch\af31505\loch\f37 editorial revisions, annotations, elaborations, or other modifica\hich\af37\dbch\af31505\loch\f37 tions 113 | \par \hich\af37\dbch\af31505\loch\f37 represent, as a whole, an original work of authorship. For the purposes 114 | \par \hich\af37\dbch\af31505\loch\f37 of this License, Derivative Works shall not include works that remain 115 | \par \hich\af37\dbch\af31505\loch\f37 separable from, or merely link (or bind by name) to the interfaces of, 116 | \par \hich\af37\dbch\af31505\loch\f37 the Work and \hich\af37\dbch\af31505\loch\f37 Derivative Works thereof. 117 | \par 118 | \par \hich\af37\dbch\af31505\loch\f37 "Contribution" shall mean any work of authorship, including 119 | \par \hich\af37\dbch\af31505\loch\f37 the original version of the Work and any modifications or additions 120 | \par \hich\af37\dbch\af31505\loch\f37 to that Work or Derivative Works thereof, that is intentionally 121 | \par \hich\af37\dbch\af31505\loch\f37 submitted to \hich\af37\dbch\af31505\loch\f37 Licensor for inclusion in the Work by the copyright owner 122 | \par \hich\af37\dbch\af31505\loch\f37 or by an individual or Legal Entity authorized to submit on behalf of 123 | \par \hich\af37\dbch\af31505\loch\f37 the copyright owner. For the purposes of this definition, "submitted" 124 | \par \hich\af37\dbch\af31505\loch\f37 means any form of electronic, verbal, or\hich\af37\dbch\af31505\loch\f37 written communication sent 125 | \par \hich\af37\dbch\af31505\loch\f37 to the Licensor or its representatives, including but not limited to 126 | \par \hich\af37\dbch\af31505\loch\f37 communication on electronic mailing lists, source code control systems, 127 | \par \hich\af37\dbch\af31505\loch\f37 and issue tracking systems that are managed by, or on behalf of, the 128 | \par \hich\af37\dbch\af31505\loch\f37 Licensor for the purpose of discussing and improving the Work, but 129 | \par \hich\af37\dbch\af31505\loch\f37 excluding communication that is conspicuously marked or otherwise 130 | \par \hich\af37\dbch\af31505\loch\f37 designated in writing by the copyright owner as "Not a Contribution." 131 | \par 132 | \par \hich\af37\dbch\af31505\loch\f37 "Contributor" shall mean Lic\hich\af37\dbch\af31505\loch\f37 ensor and any individual or Legal Entity 133 | \par \hich\af37\dbch\af31505\loch\f37 on behalf of whom a Contribution has been received by Licensor and 134 | \par \hich\af37\dbch\af31505\loch\f37 subsequently incorporated within the Work. 135 | \par 136 | \par \hich\af37\dbch\af31505\loch\f37 2. Grant of Copyright License. Subject to the terms and conditions of 137 | \par \hich\af37\dbch\af31505\loch\f37 this License,\hich\af37\dbch\af31505\loch\f37 each Contributor hereby grants to You a perpetual, 138 | \par \hich\af37\dbch\af31505\loch\f37 worldwide, non-exclusive, no-charge, royalty-free, irrevocable 139 | \par \hich\af37\dbch\af31505\loch\f37 copyright license to reproduce, prepare Derivative Works of, 140 | \par \hich\af37\dbch\af31505\loch\f37 publicly display, publicly perform, sublicense, and distribute\hich\af37\dbch\af31505\loch\f37 the 141 | \par \hich\af37\dbch\af31505\loch\f37 Work and such Derivative Works in Source or Object form. 142 | \par 143 | \par \hich\af37\dbch\af31505\loch\f37 3. Grant of Patent License. Subject to the terms and conditions of 144 | \par \hich\af37\dbch\af31505\loch\f37 this License, each Contributor hereby grants to You a perpetual, 145 | \par \hich\af37\dbch\af31505\loch\f37 worldwide, non-exclusive, no-charge, roy\hich\af37\dbch\af31505\loch\f37 alty-free, irrevocable 146 | \par \hich\af37\dbch\af31505\loch\f37 (except as stated in this section) patent license to make, have made, 147 | \par \hich\af37\dbch\af31505\loch\f37 use, offer to sell, sell, import, and otherwise transfer the Work, 148 | \par \hich\af37\dbch\af31505\loch\f37 where such license applies only to those patent claims licensable 149 | \par \hich\af37\dbch\af31505\loch\f37 by suc\hich\af37\dbch\af31505\loch\f37 h Contributor that are necessarily infringed by their 150 | \par \hich\af37\dbch\af31505\loch\f37 Contribution(s) alone or by combination of their Contribution(s) 151 | \par \hich\af37\dbch\af31505\loch\f37 with the Work to which such Contribution(s) was submitted. If You 152 | \par \hich\af37\dbch\af31505\loch\f37 institute patent litigation against any entity (including a 153 | \par \hich\af37\dbch\af31505\loch\f37 cross-claim or counterclaim in a lawsuit) alleging that the Work 154 | \par \hich\af37\dbch\af31505\loch\f37 or a Contribution incorporated within the Work constitutes direct 155 | \par \hich\af37\dbch\af31505\loch\f37 or contributory patent infringement, then\hich\af37\dbch\af31505\loch\f37 any patent licenses 156 | \par \hich\af37\dbch\af31505\loch\f37 granted to You under this License for that Work shall terminate 157 | \par \hich\af37\dbch\af31505\loch\f37 as of the date such litigation is filed. 158 | \par 159 | \par \hich\af37\dbch\af31505\loch\f37 4. Redistribution. You may reproduce and distribute copies of the 160 | \par \hich\af37\dbch\af31505\loch\f37 Work or Derivative Works thereof in any me\hich\af37\dbch\af31505\loch\f37 dium, with or without 161 | \par \hich\af37\dbch\af31505\loch\f37 modifications, and in Source or Object form, provided that You 162 | \par \hich\af37\dbch\af31505\loch\f37 meet the following conditions: 163 | \par 164 | \par \hich\af37\dbch\af31505\loch\f37 (a) You must give any other recipients of the Work or 165 | \par \hich\af37\dbch\af31505\loch\f37 Derivative Works a copy of this License; and 166 | \par 167 | \par \hich\af37\dbch\af31505\loch\f37 (b) Y\hich\af37\dbch\af31505\loch\f37 ou must cause any modified files to carry prominent notices 168 | \par \hich\af37\dbch\af31505\loch\f37 stating that You changed the files; and 169 | \par 170 | \par \hich\af37\dbch\af31505\loch\f37 (c) You must retain, in the Source form of any Derivative Works 171 | \par \hich\af37\dbch\af31505\loch\f37 that You distribute, all copyright, patent, trademark, and 172 | \par \hich\af37\dbch\af31505\loch\f37 \hich\af37\dbch\af31505\loch\f37 attribution notices from the Source form of the Work, 173 | \par \hich\af37\dbch\af31505\loch\f37 excluding those notices that do not pertain to any part of 174 | \par \hich\af37\dbch\af31505\loch\f37 the Derivative Works; and 175 | \par 176 | \par \hich\af37\dbch\af31505\loch\f37 (d) If the Work includes a "NOTICE" text file as part of its 177 | \par \hich\af37\dbch\af31505\loch\f37 distribution, t\hich\af37\dbch\af31505\loch\f37 hen any Derivative Works that You distribute must 178 | \par \hich\af37\dbch\af31505\loch\f37 include a readable copy of the attribution notices contained 179 | \par \hich\af37\dbch\af31505\loch\f37 within such NOTICE file, excluding those notices that do not 180 | \par \hich\af37\dbch\af31505\loch\f37 pertain to any part of the Derivative Works, in at least one 181 | \par \hich\af37\dbch\af31505\loch\f37 of the following places: within a NOTICE text file distributed 182 | \par \hich\af37\dbch\af31505\loch\f37 as part of the Derivative Works; within the Source form or 183 | \par \hich\af37\dbch\af31505\loch\f37 documentation, if provided along \hich\af37\dbch\af31505\loch\f37 with the Derivative Works; or, 184 | \par \hich\af37\dbch\af31505\loch\f37 within a display generated by the Derivative Works, if and 185 | \par \hich\af37\dbch\af31505\loch\f37 wherever such third-party notices normally appear. The contents 186 | \par \hich\af37\dbch\af31505\loch\f37 of the NOTICE file are for informational purposes only and 187 | \par \hich\af37\dbch\af31505\loch\f37 do \hich\af37\dbch\af31505\loch\f37 not modify the License. You may add Your own attribution 188 | \par \hich\af37\dbch\af31505\loch\f37 notices within Derivative Works that You distribute, alongside 189 | \par \hich\af37\dbch\af31505\loch\f37 or as an addendum to the NOTICE text from the Work, provided 190 | \par \hich\af37\dbch\af31505\loch\f37 that such additional attribution notices cann\hich\af37\dbch\af31505\loch\f37 ot be construed 191 | \par \hich\af37\dbch\af31505\loch\f37 as modifying the License. 192 | \par 193 | \par \hich\af37\dbch\af31505\loch\f37 You may add Your own copyright statement to Your modifications and 194 | \par \hich\af37\dbch\af31505\loch\f37 may provide additional or different license terms and conditions 195 | \par \hich\af37\dbch\af31505\loch\f37 for use, reproduction, or distribution of Your modifi\hich\af37\dbch\af31505\loch\f37 cations, or 196 | \par \hich\af37\dbch\af31505\loch\f37 for any such Derivative Works as a whole, provided Your use, 197 | \par \hich\af37\dbch\af31505\loch\f37 reproduction, and distribution of the Work otherwise complies with 198 | \par \hich\af37\dbch\af31505\loch\f37 the conditions stated in this License. 199 | \par 200 | \par \hich\af37\dbch\af31505\loch\f37 5. Submission of Contributions. Unless You explicitly s\hich\af37\dbch\af31505\loch\f37 tate otherwise, 201 | \par \hich\af37\dbch\af31505\loch\f37 any Contribution intentionally submitted for inclusion in the Work 202 | \par \hich\af37\dbch\af31505\loch\f37 by You to the Licensor shall be under the terms and conditions of 203 | \par \hich\af37\dbch\af31505\loch\f37 this License, without any additional terms or conditions. 204 | \par \hich\af37\dbch\af31505\loch\f37 Notwithstanding the above\hich\af37\dbch\af31505\loch\f37 , nothing herein shall supersede or modify 205 | \par \hich\af37\dbch\af31505\loch\f37 the terms of any separate license agreement you may have executed 206 | \par \hich\af37\dbch\af31505\loch\f37 with Licensor regarding such Contributions. 207 | \par 208 | \par \hich\af37\dbch\af31505\loch\f37 6. Trademarks. This License does not grant permission to use the trade 209 | \par \hich\af37\dbch\af31505\loch\f37 names, trademarks, service marks, or product names of the Licensor, 210 | \par \hich\af37\dbch\af31505\loch\f37 except as required for reasonable and customary use in describing the 211 | \par \hich\af37\dbch\af31505\loch\f37 origin of the Work and reproducing the content of the NOTICE file. 212 | \par 213 | \par \hich\af37\dbch\af31505\loch\f37 7. Disclaimer of Warranty. Un\hich\af37\dbch\af31505\loch\f37 less required by applicable law or 214 | \par \hich\af37\dbch\af31505\loch\f37 agreed to in writing, Licensor provides the Work (and each 215 | \par \hich\af37\dbch\af31505\loch\f37 Contributor provides its Contributions) on an "AS IS" BASIS, 216 | \par \hich\af37\dbch\af31505\loch\f37 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 217 | \par \hich\af37\dbch\af31505\loch\f37 implied, incl\hich\af37\dbch\af31505\loch\f37 uding, without limitation, any warranties or conditions 218 | \par \hich\af37\dbch\af31505\loch\f37 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 219 | \par \hich\af37\dbch\af31505\loch\f37 PARTICULAR PURPOSE. You are solely responsible for determining the 220 | \par \hich\af37\dbch\af31505\loch\f37 appropriateness of using or redistributing the Work a\hich\af37\dbch\af31505\loch\f37 nd assume any 221 | \par \hich\af37\dbch\af31505\loch\f37 risks associated with Your exercise of permissions under this License. 222 | \par 223 | \par \hich\af37\dbch\af31505\loch\f37 8. Limitation of Liability. In no event and under no legal theory, 224 | \par \hich\af37\dbch\af31505\loch\f37 whether in tort (including negligence), contract, or otherwise, 225 | \par \hich\af37\dbch\af31505\loch\f37 unless required by\hich\af37\dbch\af31505\loch\f37 applicable law (such as deliberate and grossly 226 | \par \hich\af37\dbch\af31505\loch\f37 negligent acts) or agreed to in writing, shall any Contributor be 227 | \par \hich\af37\dbch\af31505\loch\f37 liable to You for damages, including any direct, indirect, special, 228 | \par \hich\af37\dbch\af31505\loch\f37 incidental, or consequential damages of any character ar\hich\af37\dbch\af31505\loch\f37 ising as a 229 | \par \hich\af37\dbch\af31505\loch\f37 result of this License or out of the use or inability to use the 230 | \par \hich\af37\dbch\af31505\loch\f37 Work (including but not limited to damages for loss of goodwill, 231 | \par \hich\af37\dbch\af31505\loch\f37 work stoppage, computer failure or malfunction, or any and all 232 | \par \hich\af37\dbch\af31505\loch\f37 other commercial damages or \hich\af37\dbch\af31505\loch\f37 losses), even if such Contributor 233 | \par \hich\af37\dbch\af31505\loch\f37 has been advised of the possibility of such damages. 234 | \par 235 | \par \hich\af37\dbch\af31505\loch\f37 9. Accepting Warranty or Additional Liability. While redistributing 236 | \par \hich\af37\dbch\af31505\loch\f37 the Work or Derivative Works thereof, You may choose to offer, 237 | \par \hich\af37\dbch\af31505\loch\f37 and charge a fee for, acceptance of support, warranty, indemnity, 238 | \par \hich\af37\dbch\af31505\loch\f37 or other liability obligations and/or rights consistent with this 239 | \par \hich\af37\dbch\af31505\loch\f37 License. However, in accepting such obligations, You may act only 240 | \par \hich\af37\dbch\af31505\loch\f37 on Your own behalf and on Your sol\hich\af37\dbch\af31505\loch\f37 e responsibility, not on behalf 241 | \par \hich\af37\dbch\af31505\loch\f37 of any other Contributor, and only if You agree to indemnify, 242 | \par \hich\af37\dbch\af31505\loch\f37 defend, and hold each Contributor harmless for any liability 243 | \par \hich\af37\dbch\af31505\loch\f37 incurred by, or claims asserted against, such Contributor by reason 244 | \par \hich\af37\dbch\af31505\loch\f37 of your a\hich\af37\dbch\af31505\loch\f37 ccepting any such warranty or additional liability.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \f37\lang9\langfe1033\langnp9\insrsid12277323 245 | \par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a 246 | 9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad 247 | 5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 248 | b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 249 | 0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 250 | a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f 251 | c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 252 | 0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 253 | a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 254 | 6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b 255 | 4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b 256 | 4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100aa5225dfc60600008b1a0000160000007468656d652f7468656d652f 257 | 7468656d65312e786d6cec595d8bdb46147d2ff43f08bd3bfe92fcb1c41b6cd9ceb6d94d42eca4e4716c8fadc98e344633de8d0981923c160aa569e943037deb 258 | 43691b48a02fe9afd936a54d217fa17746b63c638fbb9b2585a5640d8b343af7ce997bafce1d4997afdc8fa87384134e58dc708b970aae83e3211b9178d2706f 259 | f7bbb99aeb7081e211a22cc60d778eb97b65f7c30f2ea31d11e2083b601ff31dd4704321a63bf93c1fc230e297d814c7706dcc920809384d26f951828ec16f44 260 | f3a542a1928f10895d274611b8bd311e932176fad2a5bbbb74dea1701a0b2e078634e949d7d8b050d8d1615122f89c0734718e106db830cf881df7f17de13a14 261 | 7101171a6e41fdb9f9ddcb79b4b330a2628bad66d7557f0bbb85c1e8b0a4e64c26836c52cff3bd4a33f3af00546ce23ad54ea553c9fc29001a0e61a52917dda7 262 | dfaab7dafe02ab81d2438bef76b55d2e1a78cd7f798373d3973f03af40a97f6f03dfed06104503af4029dedfc07b5eb51478065e81527c65035f2d34db5ed5c0 263 | 2b5048497cb8812ef89572b05c6d061933ba6785d77daf5b2d2d9caf50500d5975c929c62c16db6a2d42f758d2058004522448ec88f9148fd110aa3840940c12 264 | e2ec93490885374531e3305c2815ba8532fc973f4f1da988a01d8c346bc90b98f08d21c9c7e1c3844c45c3fd18bcba1ae4cdcb1fdfbc7cee9c3c7a71f2e89793 265 | c78f4f1efd9c3a32acf6503cd1ad5e7fffc5df4f3f75fe7afeddeb275fd9f15cc7fffed367bffdfaa51d082b5d85e0d5d7cffe78f1ecd5379ffff9c3130bbc99 266 | a0810eef930873e73a3e766eb10816a6426032c783e4ed2cfa2122ba45339e701423398bc57f478406fafa1c5164c1b5b019c13b09488c0d787576cf20dc0b93 267 | 9920168fd7c2c8001e30465b2cb146e19a9c4b0b737f164fec9327331d770ba123dbdc018a8dfc766653d05662731984d8a07993a258a0098eb170e4357688b1 268 | 6575770931e27a408609e36c2c9cbbc46921620d499f0c8c6a5a19ed9108f232b711847c1bb139b8e3b418b5adba8d8f4c24dc15885ac8f73135c27815cd048a 269 | 6c2efb28a27ac0f791086d247bf364a8e33a5c40a6279832a733c29cdb6c6e24b05e2de9d7405eec693fa0f3c84426821cda7cee23c674649b1d06218aa6366c 270 | 8fc4a18efd881f428922e7261336f80133ef10790e7940f1d674df21d848f7e96a701b9455a7b42a107965965872791533a37e7b733a4658490d08bfa1e71189 271 | 4f15f73559f7ff5b5907217df5ed53cbaa2eaaa0371362bda3f6d6647c1b6e5dbc03968cc8c5d7ee369ac53731dc2e9b0decbd74bf976ef77f2fdddbeee7772f 272 | d82b8d06f9965bc574abae36eed1d67dfb9850da13738af7b9daba73e84ca32e0c4a3bf5cc8ab3e7b8690887f24e86090cdc2441cac64998f88488b017a229ec 273 | ef8bae7432e10bd713ee4c19876dbf1ab6fa96783a8b0ed8287d5c2d16e5a3692a1e1c89d578c1cfc6e15143a4e84a75f50896b9576c27ea51794940dabe0d09 274 | 6d329344d942a2ba1c9441520fe610340b09b5b277c2a26e615193ee97a9da6001d4b2acc0d6c9810d57c3f53d30012378a242148f649ed2542fb3ab92f92e33 275 | bd2d984605c03e625901ab4cd725d7adcb93ab4b4bed0c99364868e566925091513d8c87688417d52947cf42e36d735d5fa5d4a02743a1e683d25ad1a8d6fe8d 276 | c579730d76ebda40635d2968ec1c37dc4ad9879219a269c31dc3633f1c4653a81d2eb7bc884ee0ddd95024e90d7f1e6599265cb4110fd3802bd149d520220227 277 | 0e2551c395cbcfd24063a5218a5bb104827061c9d541562e1a3948ba99643c1ee3a1d0d3ae8dc848a7a7a0f0a95658af2af3f383a5259b41ba7be1e8d819d059 278 | 720b4189f9d5a20ce0887078fb534ca33922f03a3313b255fdad35a685eceaef13550da5e3884e43b4e828ba98a77025e5191d7596c5403b5bac1902aa8564d1 279 | 080713d960f5a01add34eb1a2987ad5df7742319394d34573dd35015d935ed2a66ccb06c036bb13c5f93d7582d430c9aa677f854bad725b7bed4bab57d42d625 280 | 20e059fc2c5df70c0d41a3b69acca026196fcab0d4ecc5a8d93b960b3c85da599a84a6fa95a5dbb5b8653dc23a1d0c9eabf383dd7ad5c2d078b9af549156df3d 281 | f44f136c700fc4a30d2f81675470954af8f09020d810f5d49e24950db845ee8bc5ad0147ce2c210df741c16f7a41c90f72859adfc97965af90abf9cd72aee9fb 282 | e562c72f16daadd243682c228c8a7efacda50bafa2e87cf1e5458d6f7c7d89966fdb2e0d599467eaeb4a5e11575f5f8aa5ed5f5f1c02a2f3a052ead6cbf55625 283 | 572f37bb39afddaae5ea41a5956b57826abbdb0efc5abdfbd0758e14d86b9603afd2a9e52ac520c8799582a45fabe7aa5ea9d4f4aacd5ac76b3e5c6c6360e5a9 284 | 7c2c6201e155bc76ff010000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f7468656d652f5f72656c732f 285 | 7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be 286 | 9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980 287 | ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5b 288 | babac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000000000000000000000005b436f6e74656e 289 | 745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000000000300100005f72656c732f 290 | 2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000190200007468656d652f7468656d652f74 291 | 68656d654d616e616765722e786d6c504b01022d0014000600080000002100aa5225dfc60600008b1a00001600000000000000000000000000d6020000746865 292 | 6d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b0100002700000000000000000000000000d00900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000cb0a00000000} 293 | {\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d 294 | 617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 295 | 6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 296 | 656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} 297 | {\*\latentstyles\lsdstimax375\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1; 298 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; 299 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7; 300 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1; 301 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5; 302 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9; 303 | \lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; 304 | \lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; 305 | \lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; 306 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; 307 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; 308 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; 309 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; 310 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; 311 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; 312 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; 313 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; 314 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; 315 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; 316 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; 317 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; 318 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; 319 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; 320 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; 321 | \lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; 322 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; 323 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; 324 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; 325 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1; 326 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid; 327 | \lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid; 328 | \lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2; 329 | \lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1; 330 | \lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1; 331 | \lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1; 332 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1; 333 | \lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2; 334 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2; 335 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2; 336 | \lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3; 337 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3; 338 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3; 339 | \lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4; 340 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4; 341 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4; 342 | \lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5; 343 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; 344 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; 345 | \lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; 346 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; 347 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; 348 | \lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; 349 | \lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; 350 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; 351 | \lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; 352 | \lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; 353 | \lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; 354 | \lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; 355 | \lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; 356 | \lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; 357 | \lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; 358 | \lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; 359 | \lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; 360 | \lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; 361 | \lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; 362 | \lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; 363 | \lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; 364 | \lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; 365 | \lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; 366 | \lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; 367 | \lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; 368 | \lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; 369 | \lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; 370 | \lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; 371 | \lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; 372 | \lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; 373 | \lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; 374 | \lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; 375 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;}}{\*\datastore 010500000200000018000000 376 | 4d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 377 | d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 378 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 379 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 380 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 381 | fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 382 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 383 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 384 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 385 | ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000d067 386 | 505a5700d301feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 387 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 388 | 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 389 | 0000000000000000000000000000000000000000000000000105000000000000}} --------------------------------------------------------------------------------