diff options
| author | Leitwolf11 | 2026-10-29 09:53:57 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-29 09:53:57 +0300 |
| commit | 58fa0d19d223aaebf9973ebd446c4bb855351ef9 (patch) | |
| tree | 6fce1c3d9f2255e80ec92c3040e7280c0ce131ae /apps/api/ViGet/JavaProperties.cs | |
| parent | f22418235a0a87bc05b3bf9079fcdefba002d3b2 (diff) | |
| download | progmaweb-58fa0d19d223aaebf9973ebd446c4bb855351ef9.tar.gz | |
Add the Tazel plugin catalog of ViGet to the API
ViGet opens in stages: first only Progmasoft publishes Tazel plugins, later
every account. This adds the catalog and the server-side publishing for the
first stage; nothing publishes through the API yet.
- ViGet/: the catalog of Tazel plugins (in memory, or in PostgreSQL beside
the accounts with its own numbered schema steps), the descriptor read from
the JAR (META-INF/tazel-plugin.properties, with a reader of the Java
properties format, and the ProjectPlugin service entry), and publishing:
the publisher must be in ViGet:TazelPublishers (Progmasoft for now) and an
existing account of that exact name, a version is never replaced, names
that differ only in letter case are refused, and ViGet needs a
description, an author and a license. The JAR is kept at
dslplugins/tazel/<Publisher>/<Name>/<Version>/<Name>-<Version>.jar below
ViGet:ArtifactRoot.
- Read endpoints under /api/v1/viget/dslplugins/tazel: the list, a plugin,
and index.json, the index Tazel reads.
- 'viget publish-tazel-plugin <jar>' and 'viget yank-tazel-plugin' run with
the configuration of the service instead of the service.
- The account store can find an account by name.
The pages, the Nginx routes and the deployment follow.
Diffstat (limited to 'apps/api/ViGet/JavaProperties.cs')
| -rw-r--r-- | apps/api/ViGet/JavaProperties.cs | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/apps/api/ViGet/JavaProperties.cs b/apps/api/ViGet/JavaProperties.cs new file mode 100644 index 0000000..e55991e --- /dev/null +++ b/apps/api/ViGet/JavaProperties.cs @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +using System.Globalization; +using System.Text; + +namespace Progmasoft.Progmaweb.Api.ViGet; + +/// <summary>Reads the text of a Java <c>.properties</c> file, as <c>java.util.Properties.load</c> does.</summary> +/// <remarks> +/// A logical line may continue over several lines with a trailing backslash; <c>#</c> and <c>!</c> start a comment +/// line; a key ends at the first unescaped <c>=</c>, <c>:</c> or white space; and <c>\t</c>, <c>\n</c>, <c>\r</c>, +/// <c>\f</c> and <c>\uXXXX</c> are escapes, while a backslash before any other character stands for that character. +/// A later line with the same key replaces an earlier one. +/// </remarks> +internal static class JavaProperties +{ + /// <summary>Reads the properties of a text.</summary> + /// <param name="text">The text of the file, already decoded.</param> + /// <returns>The properties by key.</returns> + /// <exception cref="FormatException">A <c>\u</c> escape is not followed by four hexadecimal digits.</exception> + public static IReadOnlyDictionary<string, string> Parse(string text) + { + Dictionary<string, string> properties = new(StringComparer.Ordinal); + foreach (string line in LogicalLines(text)) + { + int index = 0; + while (index < line.Length && IsBlank(line[index])) + { + index++; + } + + if (index == line.Length || line[index] is '#' or '!') + { + continue; + } + + int keyStart = index; + while (index < line.Length && !IsKeyEnd(line[index])) + { + index += line[index] == '\\' ? 2 : 1; + } + + string key = Unescape(line[keyStart..Math.Min(index, line.Length)]); + while (index < line.Length && IsBlank(line[index])) + { + index++; + } + + if (index < line.Length && line[index] is '=' or ':') + { + index++; + while (index < line.Length && IsBlank(line[index])) + { + index++; + } + } + + properties[key] = Unescape(line[index..]); + } + + return properties; + } + + /// <summary>Joins the physical lines that a trailing backslash continues.</summary> + private static IEnumerable<string> LogicalLines(string text) + { + string[] lines = text.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n').Split('\n'); + StringBuilder current = new(); + bool continuing = false; + foreach (string physical in lines) + { + // A continued line loses the white space it starts with. + string line = continuing ? physical.TrimStart(' ', '\t', '\f') : physical; + int backslashes = 0; + for (int position = line.Length - 1; position >= 0 && line[position] == '\\'; position--) + { + backslashes++; + } + + continuing = backslashes % 2 == 1; + current.Append(continuing ? line[..^1] : line); + if (!continuing) + { + yield return current.ToString(); + current.Clear(); + } + } + + if (current.Length > 0) + { + yield return current.ToString(); + } + } + + private static bool IsBlank(char character) => character is ' ' or '\t' or '\f'; + + private static bool IsKeyEnd(char character) => character is '=' or ':' || IsBlank(character); + + private static string Unescape(string text) + { + StringBuilder result = new(text.Length); + for (int index = 0; index < text.Length; index++) + { + char character = text[index]; + if (character != '\\' || index + 1 == text.Length) + { + result.Append(character); + continue; + } + + char escaped = text[++index]; + switch (escaped) + { + case 't': + result.Append('\t'); + break; + case 'n': + result.Append('\n'); + break; + case 'r': + result.Append('\r'); + break; + case 'f': + result.Append('\f'); + break; + case 'u': + if (text.Length - index - 1 < 4 || + !int.TryParse( + text.AsSpan(index + 1, 4), + NumberStyles.AllowHexSpecifier, + CultureInfo.InvariantCulture, + out int code)) + { + throw new FormatException("A \\u escape needs four hexadecimal digits."); + } + + result.Append((char)code); + index += 4; + break; + default: + result.Append(escaped); + break; + } + } + + return result.ToString(); + } +} |