mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 20:27:21 +00:00
Added importing of configurations exported from AI Studio (#1007)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
f8c0232cfd
commit
22065783c2
49 files changed
+3257
-263
No files matched your search
@@ -0,0 +1,326 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>Reads one exported configuration assignment as data. No Lua state is created.</summary>
|
||||
/// <remarks>
|
||||
/// A snippet is text somebody else wrote, pasted by a user who cannot tell what it does. Running it,
|
||||
/// even in the sandbox of the plugins, would let it do whatever Lua allows: loop forever, fill the
|
||||
/// memory, or compute its values from anything within reach. Configuration plugins may run code
|
||||
/// because an organization deploys them; a snippet earns no such trust. This parser therefore
|
||||
/// accepts only what an export writes: one assignment of a table made of strings, numbers,
|
||||
/// booleans, and tables, with comments in between. Whatever it accepts, it reads as Lua would, so a
|
||||
/// snippet means the same here as in a configuration plugin. Anything else is refused, never
|
||||
/// guessed: a construct which Lua would read differently must not slip through as data.
|
||||
/// </remarks>
|
||||
public sealed class ConfigurationSnippetParser
|
||||
{
|
||||
private readonly string source;
|
||||
private int position;
|
||||
|
||||
private ConfigurationSnippetParser(string source) => this.source = source;
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationSnippetParser).Namespace, nameof(ConfigurationSnippetParser));
|
||||
|
||||
/// <summary>
|
||||
/// Reads a snippet of the form <c>CONFIG["SECTION"][#CONFIG["SECTION"]+1] = { ... }</c>.
|
||||
/// </summary>
|
||||
/// <param name="source">The pasted snippet.</param>
|
||||
/// <param name="type">The type of configuration object the snippet must describe.</param>
|
||||
/// <param name="table">The table the snippet assigns, when it could be read.</param>
|
||||
/// <param name="issue">Why the snippet could not be read, otherwise empty.</param>
|
||||
/// <returns>True when the snippet could be read.</returns>
|
||||
public static bool TryParse(string source, PluginConfigurationObjectType type, out LuaTable table, out string issue)
|
||||
{
|
||||
table = new LuaTable();
|
||||
issue = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(source) || source.Length > 1_000_000)
|
||||
{
|
||||
issue = TB("Please paste one exported configuration of at most 1 MB.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var expectedSection = PluginConfigurationObjectDescriptors.Get(type).LuaTableName;
|
||||
var parser = new ConfigurationSnippetParser(source);
|
||||
parser.ExpectWord("CONFIG");
|
||||
var section = parser.ReadBracketedString();
|
||||
if (section != expectedSection)
|
||||
throw new FormatException(PluginConfigurationObjectDescriptors.TryGet(section, out var otherDescriptor)
|
||||
? string.Format(TB("The pasted configuration describes the type '{0}', but only the type '{1}' can be imported here."), otherDescriptor.Type.GetName(), type.GetName())
|
||||
: string.Format(TB("The pasted configuration does not describe the type '{0}', which can be imported here."), type.GetName()));
|
||||
|
||||
parser.Expect('[');
|
||||
parser.Expect('#');
|
||||
parser.ExpectWord("CONFIG");
|
||||
if (parser.ReadBracketedString() != expectedSection)
|
||||
throw new FormatException(TB("The pasted configuration names two different types of configuration."));
|
||||
parser.Expect('+');
|
||||
parser.Expect('1');
|
||||
parser.Expect(']');
|
||||
parser.Expect('=');
|
||||
table = parser.ReadTable(0);
|
||||
parser.SkipTrivia();
|
||||
if (parser.position != source.Length)
|
||||
throw new FormatException(TB("The pasted configuration must consist of exactly one exported item, without any further code."));
|
||||
return true;
|
||||
}
|
||||
catch (FormatException exception)
|
||||
{
|
||||
issue = exception.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private LuaTable ReadTable(int depth)
|
||||
{
|
||||
if (depth > 32)
|
||||
throw new FormatException(TB("The pasted configuration contains too many nested tables."));
|
||||
this.Expect('{');
|
||||
var table = new LuaTable();
|
||||
var arrayIndex = 1;
|
||||
var names = new HashSet<string>(StringComparer.Ordinal);
|
||||
while (true)
|
||||
{
|
||||
this.SkipTrivia();
|
||||
if (this.Take('}'))
|
||||
return table;
|
||||
|
||||
// "[[" or "[==[" opens a long string, which is an array value rather than a bracketed key:
|
||||
if (this.Peek() == '[' && !this.IsLongStringStart())
|
||||
{
|
||||
this.position++;
|
||||
var key = this.ReadString();
|
||||
this.Expect(']');
|
||||
this.Expect('=');
|
||||
if (!names.Add(key))
|
||||
throw new FormatException(string.Format(TB("The field '{0}' occurs more than once."), key));
|
||||
table[key] = this.ReadValue(depth + 1);
|
||||
}
|
||||
else
|
||||
table[arrayIndex++] = this.ReadValue(depth + 1);
|
||||
|
||||
this.SkipTrivia();
|
||||
if (this.Take('}'))
|
||||
return table;
|
||||
if (!this.Take(',') && !this.Take(';'))
|
||||
throw new FormatException(string.Format(TB("Expected a comma or closing brace at character {0}."), this.position + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private LuaValue ReadValue(int depth)
|
||||
{
|
||||
this.SkipTrivia();
|
||||
if (this.Peek() == '{')
|
||||
return this.ReadTable(depth);
|
||||
if (this.Peek() is '"' or '\'' || this.Peek() == '[')
|
||||
return this.ReadString();
|
||||
if (this.TakeWord("true"))
|
||||
return true;
|
||||
if (this.TakeWord("false"))
|
||||
return false;
|
||||
|
||||
// In Lua, nil leaves a field out, and in a list it ends what the parsers read of it:
|
||||
if (this.TakeWord("nil"))
|
||||
throw new FormatException(string.Format(TB("The value nil at character {0} is not supported. Leave the field out instead."), this.position - 2));
|
||||
|
||||
var start = this.position;
|
||||
if (this.Peek() == '-')
|
||||
this.position++;
|
||||
while (char.IsAsciiDigit(this.Peek()))
|
||||
this.position++;
|
||||
if (this.Peek() == '.')
|
||||
{
|
||||
this.position++;
|
||||
while (char.IsAsciiDigit(this.Peek()))
|
||||
this.position++;
|
||||
}
|
||||
if (this.position > start && double.TryParse(this.source[start..this.position], NumberStyles.Float, CultureInfo.InvariantCulture, out var number) && double.IsFinite(number))
|
||||
return number;
|
||||
throw new FormatException(string.Format(TB("At character {0}, the pasted configuration contains code. Only plain values such as texts, numbers, true, and false are allowed."), start + 1));
|
||||
}
|
||||
|
||||
private bool IsLongStringStart() => this.IsLongBracketAt(this.position);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a long bracket opens at this index: '[', any number of '=', and '[' again.
|
||||
/// </summary>
|
||||
private bool IsLongBracketAt(int index)
|
||||
{
|
||||
if (index >= this.source.Length || this.source[index] != '[')
|
||||
return false;
|
||||
|
||||
index++;
|
||||
while (index < this.source.Length && this.source[index] == '=')
|
||||
index++;
|
||||
|
||||
return index < this.source.Length && this.source[index] == '[';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips one line break, which Lua counts as one whether it is \n, \r, \r\n, or \n\r.
|
||||
/// </summary>
|
||||
private void SkipLineBreak()
|
||||
{
|
||||
var first = this.Peek();
|
||||
if (first is not ('\r' or '\n'))
|
||||
return;
|
||||
|
||||
this.position++;
|
||||
var second = this.Peek();
|
||||
if (second is '\r' or '\n' && second != first)
|
||||
this.position++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns every line break of a long string into \n, as Lua does.
|
||||
/// </summary>
|
||||
private static string NormalizeLineBreaks(string text)
|
||||
{
|
||||
if (!text.Contains('\r'))
|
||||
return text;
|
||||
|
||||
var builder = new StringBuilder(text.Length);
|
||||
for (var i = 0; i < text.Length; i++)
|
||||
{
|
||||
var c = text[i];
|
||||
if (c is not ('\r' or '\n'))
|
||||
{
|
||||
builder.Append(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append('\n');
|
||||
if (i + 1 < text.Length && text[i + 1] is '\r' or '\n' && text[i + 1] != c)
|
||||
i++;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string ReadBracketedString()
|
||||
{
|
||||
this.Expect('[');
|
||||
var result = this.ReadString();
|
||||
this.Expect(']');
|
||||
return result;
|
||||
}
|
||||
|
||||
private string ReadString()
|
||||
{
|
||||
this.SkipTrivia();
|
||||
var quote = this.Peek();
|
||||
if (quote == '[')
|
||||
{
|
||||
this.position++;
|
||||
var equalsStart = this.position;
|
||||
while (this.Peek() == '=')
|
||||
this.position++;
|
||||
var equals = this.source[equalsStart..this.position];
|
||||
if (this.Peek() != '[')
|
||||
throw new FormatException(string.Format(TB("Expected '{0}' at character {1}."), '[', this.position + 1));
|
||||
this.position++;
|
||||
|
||||
// Like Lua, a line break right after the opening bracket is not part of the string:
|
||||
this.SkipLineBreak();
|
||||
var end = this.source.IndexOf("]" + equals + "]", this.position, StringComparison.Ordinal);
|
||||
if (end < 0)
|
||||
throw new FormatException(TB("A long string is not closed."));
|
||||
var value = NormalizeLineBreaks(this.source[this.position..end]);
|
||||
this.position = end + equals.Length + 2;
|
||||
return value;
|
||||
}
|
||||
if (quote is not ('"' or '\''))
|
||||
throw new FormatException(string.Format(TB("Expected a quoted string at character {0}."), this.position + 1));
|
||||
this.position++;
|
||||
var builder = new StringBuilder();
|
||||
while (this.position < this.source.Length)
|
||||
{
|
||||
var c = this.source[this.position++];
|
||||
if (c == quote)
|
||||
return builder.ToString();
|
||||
if (c is '\r' or '\n')
|
||||
throw new FormatException(TB("A quoted string contains a line break, which Lua does not allow."));
|
||||
if (c != '\\')
|
||||
{
|
||||
builder.Append(c);
|
||||
continue;
|
||||
}
|
||||
if (this.position == this.source.Length)
|
||||
break;
|
||||
c = this.source[this.position++];
|
||||
builder.Append(c switch
|
||||
{
|
||||
'n' => '\n', 'r' => '\r', 't' => '\t', 'a' => '\a', 'b' => '\b', 'f' => '\f', 'v' => '\v',
|
||||
'\\' => '\\', '"' => '"', '\'' => '\'',
|
||||
_ => throw new FormatException(string.Format(TB("The escape sequence {0} is not supported."), "\\" + c)),
|
||||
});
|
||||
}
|
||||
throw new FormatException(TB("A quoted string is not closed."));
|
||||
}
|
||||
|
||||
private void SkipTrivia()
|
||||
{
|
||||
while (this.position < this.source.Length)
|
||||
{
|
||||
if (char.IsWhiteSpace(this.source[this.position]))
|
||||
{
|
||||
this.position++;
|
||||
continue;
|
||||
}
|
||||
if (this.source.AsSpan(this.position).StartsWith("--"))
|
||||
{
|
||||
//
|
||||
// A block comment may end in the middle of a line and continue with data, which a
|
||||
// line comment would swallow. Rather than reading it differently than Lua does, it
|
||||
// is refused:
|
||||
//
|
||||
if (this.IsLongBracketAt(this.position + 2))
|
||||
throw new FormatException(string.Format(TB("The block comment at character {0} is not supported. Please use line comments, which start with -- and end at the end of the line."), this.position + 1));
|
||||
|
||||
this.position += 2;
|
||||
while (this.position < this.source.Length && this.source[this.position] != '\n')
|
||||
this.position++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private char Peek() => this.position < this.source.Length ? this.source[this.position] : '\0';
|
||||
|
||||
private bool Take(char c)
|
||||
{
|
||||
this.SkipTrivia();
|
||||
if (this.Peek() != c)
|
||||
return false;
|
||||
this.position++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Expect(char c)
|
||||
{
|
||||
if (!this.Take(c))
|
||||
throw new FormatException(string.Format(TB("Expected '{0}' at character {1}."), c, this.position + 1));
|
||||
}
|
||||
|
||||
private bool TakeWord(string word)
|
||||
{
|
||||
this.SkipTrivia();
|
||||
if (!this.source.AsSpan(this.position).StartsWith(word) ||
|
||||
(this.position + word.Length < this.source.Length && (char.IsLetterOrDigit(this.source[this.position + word.Length]) || this.source[this.position + word.Length] == '_')))
|
||||
return false;
|
||||
this.position += word.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ExpectWord(string word)
|
||||
{
|
||||
if (!this.TakeWord(word))
|
||||
throw new FormatException(string.Format(TB("Expected '{0}' at character {1}."), word, this.position + 1));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user