1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
// 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();
}
}
/// <summary>Whether a character is white space of the format: space, tab or form feed.</summary>
private static bool IsBlank(char character) => character is ' ' or '\t' or '\f';
/// <summary>Whether an unescaped character ends a key.</summary>
private static bool IsKeyEnd(char character) => character is '=' or ':' || IsBlank(character);
/// <summary>Resolves the escapes of a key or a value.</summary>
/// <param name="text">The text as the file writes it.</param>
/// <returns>The text it stands for.</returns>
/// <exception cref="FormatException">A <c>\u</c> escape is not followed by four hexadecimal digits.</exception>
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();
}
}
|