-
Notifications
You must be signed in to change notification settings - Fork 0
/
TermInfo.Database.cs
310 lines (267 loc) · 15.3 KB
/
TermInfo.Database.cs
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace System;
internal static partial class TermInfo
{
/// <summary>Provides a terminfo database.</summary>
internal sealed class Database
{
/// <summary>The name of the terminfo file.</summary>
private readonly string _term;
/// <summary>Raw data of the database instance.</summary>
private readonly byte[] _data;
/// <summary>The number of bytes in the names section of the database.</summary>
private readonly int _nameSectionNumBytes;
/// <summary>The number of bytes in the Booleans section of the database.</summary>
private readonly int _boolSectionNumBytes;
/// <summary>The number of integers in the numbers section of the database.</summary>
private readonly int _numberSectionNumInts;
/// <summary>The number of offsets in the strings section of the database.</summary>
private readonly int _stringSectionNumOffsets;
/// <summary>The number of bytes in the strings table of the database.</summary>
private readonly int _stringTableNumBytes;
/// <summary>Whether or not to read the number section as 32-bit integers.</summary>
private readonly bool _readAs32Bit;
/// <summary>The size of the integers on the number section.</summary>
private readonly int _sizeOfInt;
/// <summary>Extended / user-defined entries in the terminfo database.</summary>
private readonly Dictionary<string, string> _extendedStrings;
/// <summary>Initializes the database instance.</summary>
/// <param name="term">The name of the terminal.</param>
/// <param name="data">The data from the terminfo file.</param>
internal Database(string term, byte[] data)
{
_term = term;
_data = data;
const int MagicLegacyNumber = 0x11A; // magic number octal 0432 for legacy ncurses terminfo
const int Magic32BitNumber = 0x21E; // magic number octal 01036 for new ncruses terminfo
short magic = ReadInt16(data, 0);
_readAs32Bit =
magic == MagicLegacyNumber ? false :
magic == Magic32BitNumber ? true :
throw new InvalidOperationException("IO_TermInfoInvalidMagicNumber"); // magic number was not recognized. Printing the magic number in octal.
_sizeOfInt = (_readAs32Bit) ? 4 : 2;
_nameSectionNumBytes = ReadInt16(data, 2);
_boolSectionNumBytes = ReadInt16(data, 4);
_numberSectionNumInts = ReadInt16(data, 6);
_stringSectionNumOffsets = ReadInt16(data, 8);
_stringTableNumBytes = ReadInt16(data, 10);
if (_nameSectionNumBytes < 0 ||
_boolSectionNumBytes < 0 ||
_numberSectionNumInts < 0 ||
_stringSectionNumOffsets < 0 ||
_stringTableNumBytes < 0)
{
throw new InvalidOperationException("IO_TermInfoInvalid");
}
// In addition to the main section of bools, numbers, and strings, there is also
// an "extended" section. This section contains additional entries that don't
// have well-known indices, and are instead named mappings. As such, we parse
// all of this data now rather than on each request, as the mapping is fairly complicated.
// This function relies on the data stored above, so it's the last thing we run.
// (Note that the extended section also includes other Booleans and numbers, but we don't
// have any need for those now, so we don't parse them.)
int extendedBeginning = RoundUpToEven(StringsTableOffset + _stringTableNumBytes);
_extendedStrings = ParseExtendedStrings(data, extendedBeginning, _readAs32Bit) ?? new Dictionary<string, string>();
}
/// <summary>The name of the associated terminfo, if any.</summary>
public string Term { get { return _term; } }
/// <summary>The offset into data where the names section begins.</summary>
private const int NamesOffset = 12; // comes right after the header, which is always 12 bytes
/// <summary>The offset into data where the Booleans section begins.</summary>
private int BooleansOffset { get { return NamesOffset + _nameSectionNumBytes; } } // after the names section
/// <summary>The offset into data where the numbers section begins.</summary>
private int NumbersOffset { get { return RoundUpToEven(BooleansOffset + _boolSectionNumBytes); } } // after the Booleans section, at an even position
/// <summary>
/// The offset into data where the string offsets section begins. We index into this section
/// to find the location within the strings table where a string value exists.
/// </summary>
private int StringOffsetsOffset { get { return NumbersOffset + (_numberSectionNumInts * _sizeOfInt); } }
/// <summary>The offset into data where the string table exists.</summary>
private int StringsTableOffset { get { return StringOffsetsOffset + (_stringSectionNumOffsets * 2); } }
/// <summary>Gets a string from the strings section by the string's well-known index.</summary>
/// <param name="stringTableIndex">The index of the string to find.</param>
/// <returns>The string if it's in the database; otherwise, null.</returns>
public string? GetString(WellKnownStrings stringTableIndex)
{
int index = (int)stringTableIndex;
Debug.Assert(index >= 0);
if (index >= _stringSectionNumOffsets)
{
// Some terminfo files may not contain enough entries to actually
// have the requested one.
return null;
}
int tableIndex = ReadInt16(_data, StringOffsetsOffset + (index * 2));
if (tableIndex == -1)
{
// Some terminfo files may have enough entries, but may not actually
// have it filled in for this particular string.
return null;
}
return ReadString(_data, StringsTableOffset + tableIndex);
}
/// <summary>Gets a string from the extended strings section.</summary>
/// <param name="name">The name of the string as contained in the extended names section.</param>
/// <returns>The string if it's in the database; otherwise, null.</returns>
public string? GetExtendedString(string name)
{
Debug.Assert(name != null);
string? value;
return _extendedStrings.TryGetValue(name, out value) ?
value :
null;
}
/// <summary>Gets a number from the numbers section by the number's well-known index.</summary>
/// <param name="numberIndex">The index of the string to find.</param>
/// <returns>The number if it's in the database; otherwise, -1.</returns>
public int GetNumber(WellKnownNumbers numberIndex)
{
int index = (int)numberIndex;
Debug.Assert(index >= 0);
if (index >= _numberSectionNumInts)
{
// Some terminfo files may not contain enough entries to actually
// have the requested one.
return -1;
}
return ReadInt(_data, NumbersOffset + (index * _sizeOfInt), _readAs32Bit);
}
/// <summary>Parses the extended string information from the terminfo data.</summary>
/// <returns>
/// A dictionary of the name to value mapping. As this section of the terminfo isn't as well
/// defined as the earlier portions, and may not even exist, the parsing is more lenient about
/// errors, returning an empty collection rather than throwing.
/// </returns>
private static Dictionary<string, string>? ParseExtendedStrings(byte[] data, int extendedBeginning, bool readAs32Bit)
{
const int ExtendedHeaderSize = 10;
int sizeOfIntValuesInBytes = (readAs32Bit) ? 4 : 2;
if (extendedBeginning + ExtendedHeaderSize >= data.Length)
{
// Exit out as there's no extended information.
return null;
}
// Read in extended counts, and exit out if we got any incorrect info
int extendedBoolCount = ReadInt16(data, extendedBeginning);
int extendedNumberCount = ReadInt16(data, extendedBeginning + (2 * 1));
int extendedStringCount = ReadInt16(data, extendedBeginning + (2 * 2));
int extendedStringNumOffsets = ReadInt16(data, extendedBeginning + (2 * 3));
int extendedStringTableByteSize = ReadInt16(data, extendedBeginning + (2 * 4));
if (extendedBoolCount < 0 ||
extendedNumberCount < 0 ||
extendedStringCount < 0 ||
extendedStringNumOffsets < 0 ||
extendedStringTableByteSize < 0)
{
// The extended header contained invalid data. Bail.
return null;
}
// Skip over the extended bools. We don't need them now and can add this in later
// if needed. Also skip over extended numbers, for the same reason.
// Get the location where the extended string offsets begin. These point into
// the extended string table.
int extendedOffsetsStart =
extendedBeginning + // go past the normal data
ExtendedHeaderSize + // and past the extended header
RoundUpToEven(extendedBoolCount) + // and past all of the extended Booleans
(extendedNumberCount * sizeOfIntValuesInBytes); // and past all of the extended numbers
// Get the location where the extended string table begins. This area contains
// null-terminated strings.
int extendedStringTableStart =
extendedOffsetsStart +
(extendedStringCount * 2) + // and past all of the string offsets
((extendedBoolCount + extendedNumberCount + extendedStringCount) * 2); // and past all of the name offsets
// Get the location where the extended string table ends. We shouldn't read past this.
int extendedStringTableEnd =
extendedStringTableStart +
extendedStringTableByteSize;
if (extendedStringTableEnd > data.Length)
{
// We don't have enough data to parse everything. Bail.
return null;
}
// Now we need to parse all of the extended string values. These aren't necessarily
// "in order", meaning the offsets aren't guaranteed to be increasing. Instead, we parse
// the offsets in order, pulling out each string it references and storing them into our
// results list in the order of the offsets.
var values = new List<string>(extendedStringCount);
int lastEnd = 0;
for (int i = 0; i < extendedStringCount; i++)
{
int offset = extendedStringTableStart + ReadInt16(data, extendedOffsetsStart + (i * 2));
if (offset < 0 || offset >= data.Length)
{
// If the offset is invalid, bail.
return null;
}
// Add the string
int end = FindNullTerminator(data, offset);
values.Add(Encoding.ASCII.GetString(data, offset, end - offset));
// Keep track of where the last string ends. The name strings will come after that.
lastEnd = Math.Max(end, lastEnd);
}
// Now parse all of the names.
var names = new List<string>(extendedBoolCount + extendedNumberCount + extendedStringCount);
for (int pos = lastEnd + 1; pos < extendedStringTableEnd; pos++)
{
int end = FindNullTerminator(data, pos);
names.Add(Encoding.ASCII.GetString(data, pos, end - pos));
pos = end;
}
// The names are in order for the Booleans, then the numbers, and then the strings.
// Skip over the bools and numbers, and associate the names with the values.
var extendedStrings = new Dictionary<string, string>(extendedStringCount);
for (int iName = extendedBoolCount + extendedNumberCount, iValue = 0;
iName < names.Count && iValue < values.Count;
iName++, iValue++)
{
extendedStrings.Add(names[iName], values[iValue]);
}
return extendedStrings;
}
private static int RoundUpToEven(int i) { return i % 2 == 1 ? i + 1 : i; }
/// <summary>Read a 16-bit or 32-bit value from the buffer starting at the specified position.</summary>
/// <param name="buffer">The buffer from which to read.</param>
/// <param name="pos">The position at which to read.</param>
/// <param name="readAs32Bit">Whether or not to read value as 32-bit. Will read as 16-bit if set to false.</param>
/// <returns>The value read.</returns>
private static int ReadInt(byte[] buffer, int pos, bool readAs32Bit) =>
readAs32Bit ? ReadInt32(buffer, pos) : ReadInt16(buffer, pos);
/// <summary>Read a 16-bit value from the buffer starting at the specified position.</summary>
/// <param name="buffer">The buffer from which to read.</param>
/// <param name="pos">The position at which to read.</param>
/// <returns>The 16-bit value read.</returns>
private static short ReadInt16(byte[] buffer, int pos)
{
return unchecked((short)
((((int)buffer[pos + 1]) << 8) |
((int)buffer[pos] & 0xff)));
}
/// <summary>Read a 32-bit value from the buffer starting at the specified position.</summary>
/// <param name="buffer">The buffer from which to read.</param>
/// <param name="pos">The position at which to read.</param>
/// <returns>The 32-bit value read.</returns>
private static int ReadInt32(byte[] buffer, int pos)
=> BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(pos));
/// <summary>Reads a string from the buffer starting at the specified position.</summary>
/// <param name="buffer">The buffer from which to read.</param>
/// <param name="pos">The position at which to read.</param>
/// <returns>The string read from the specified position.</returns>
private static string ReadString(byte[] buffer, int pos)
{
int end = FindNullTerminator(buffer, pos);
return Encoding.ASCII.GetString(buffer, pos, end - pos);
}
/// <summary>Finds the null-terminator for a string that begins at the specified position.</summary>
private static int FindNullTerminator(byte[] buffer, int pos)
{
int i = buffer.AsSpan(pos).IndexOf((byte)'\0');
return i >= 0 ? pos + i : buffer.Length;
}
}
}