Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/libraries/System.Console/src/System.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,6 @@
Link="Common\Interop\Unix\Interop.GetControlCharacters.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.IsATty.cs"
Link="Common\Interop\Unix\Interop.IsATty.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.LSeek.cs"
Link="Common\Interop\Unix\Interop.LSeek.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Open.cs"
Link="Common\Interop\Unix\Interop.Open.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.OpenFlags.cs"
Expand Down
4 changes: 2 additions & 2 deletions src/libraries/System.Console/src/System/ConsolePal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -921,8 +921,8 @@ private static unsafe void EnsureInitializedCore()
/// <summary>Reads data from the file descriptor into the buffer.</summary>
/// <param name="fd">The file descriptor.</param>
/// <param name="buffer">The buffer to read into.</param>
/// <returns>The number of bytes read, or a negative value if there's an error.</returns>
internal static unsafe int Read(SafeFileHandle fd, Span<byte> buffer)
/// <returns>The number of bytes read, or an exception if there's an error.</returns>
private static unsafe int Read(SafeFileHandle fd, Span<byte> buffer)
{
fixed (byte* bufPtr = buffer)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal sealed class DatabaseFactory
/// The default locations in which to search for terminfo databases.
/// This is the ordering of well-known locations used by ncurses.
/// </summary>
private static readonly string[] _terminfoLocations = new string[] {
internal static readonly string[] s_terminfoLocations = {
"/etc/terminfo",
"/lib/terminfo",
"/usr/share/terminfo",
Expand All @@ -34,7 +34,7 @@ internal sealed class DatabaseFactory
/// <summary>Read the database for the specified terminal.</summary>
/// <param name="term">The identifier for the terminal.</param>
/// <returns>The database, or null if it could not be found.</returns>
private static Database? ReadDatabase(string term)
internal static Database? ReadDatabase(string term)
{
// This follows the same search order as prescribed by ncurses.
Database? db;
Expand All @@ -54,7 +54,7 @@ internal sealed class DatabaseFactory
}

// Then try a set of well-known locations.
foreach (string terminfoLocation in _terminfoLocations)
foreach (string terminfoLocation in s_terminfoLocations)
{
if ((db = ReadDatabase(term, terminfoLocation)) != null)
{
Expand Down Expand Up @@ -88,7 +88,7 @@ private static bool TryOpen(string filePath, [NotNullWhen(true)] out SafeFileHan
/// <param name="term">The identifier for the terminal.</param>
/// <param name="directoryPath">The path to the directory containing terminfo database files.</param>
/// <returns>The database, or null if it could not be found.</returns>
private static Database? ReadDatabase(string? term, string? directoryPath)
internal static Database? ReadDatabase(string? term, string? directoryPath)
{
if (string.IsNullOrEmpty(term) || string.IsNullOrEmpty(directoryPath))
{
Expand All @@ -106,8 +106,7 @@ private static bool TryOpen(string filePath, [NotNullWhen(true)] out SafeFileHan
using (fd)
{
// Read in all of the terminfo data
long termInfoLength = Interop.CheckIo(Interop.Sys.LSeek(fd, 0, Interop.Sys.SeekWhence.SEEK_END)); // jump to the end to get the file length
Interop.CheckIo(Interop.Sys.LSeek(fd, 0, Interop.Sys.SeekWhence.SEEK_SET)); // reset back to beginning
long termInfoLength = RandomAccess.GetLength(fd);
const int MaxTermInfoLength = 4096; // according to the term and tic man pages, 4096 is the terminfo file size max
const int HeaderLength = 12;
if (termInfoLength <= HeaderLength || termInfoLength > MaxTermInfoLength)
Expand All @@ -116,10 +115,17 @@ private static bool TryOpen(string filePath, [NotNullWhen(true)] out SafeFileHan
}

byte[] data = new byte[(int)termInfoLength];
if (ConsolePal.Read(fd, data) != data.Length)
long fileOffset = 0;
do
{
throw new InvalidOperationException(SR.IO_TermInfoInvalid);
}
int bytesRead = RandomAccess.Read(fd, new Span<byte>(data, (int)fileOffset, (int)(termInfoLength - fileOffset)), fileOffset);
if (bytesRead == 0)
{
throw new InvalidOperationException(SR.IO_TermInfoInvalid);
}

fileOffset += bytesRead;
} while (fileOffset < termInfoLength);

// Create the database from the data
return new Database(term, data);
Expand Down
24 changes: 23 additions & 1 deletion src/libraries/System.Console/tests/System.Console.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
<Compile Include="Timeout.cs" />
<Compile Include="ThreadSafety.cs" />
<Compile Include="XunitAssemblyAttributes.cs" />
<Compile Include="TermInfo.cs" />
<Compile Include="RedirectedStream.cs" />
<Compile Include="ReadKey.cs" />
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs"
Expand Down Expand Up @@ -54,9 +53,32 @@
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != 'windows'">
<Compile Include="CancelKeyPress.Unix.cs" />
<Compile Include="NonStandardConfiguration.Unix.cs" />
<Compile Include="TermInfo.Unix.cs" />
<Compile Include="..\src\System\TermInfo.cs" Link="src\System\TermInfo.cs" />
<Compile Include="..\src\System\TermInfo.DatabaseFactory.cs" Link="src\System\TermInfo.DatabaseFactory.cs" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, in other libraries, e.g. all of the System.Net.* libs, System.Text.RegularExpressions, etc., we've split the test projects into FunctionalTests and UnitTests, where the difference is the FunctionalTests are written only in terms of the public surface area exposed by the library under test, and the UnitTests include source from the target library in order to test internals directly.

<Compile Include="$(CoreLibSharedDir)System\IO\PersistedFiles.Unix.cs"
Link="Common\System\IO\PersistedFiles.Unix.cs" />
<Compile Include="$(CoreLibSharedDir)System\IO\PersistedFiles.Names.Unix.cs"
Link="Common\System\IO\PersistedFiles.Names.Unix.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.cs"
Link="Common\System\Text\ValueStringBuilder.cs" />
<Compile Include="$(CommonPath)System\Text\ValueStringBuilder.AppendSpanFormattable.cs"
Link="Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs"
Link="Common\Interop\Windows\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Errors.cs"
Link="Common\Interop\Unix\Interop.Errors.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Open.cs"
Link="Common\Interop\Unix\Interop.Open.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.OpenFlags.cs"
Link="Common\Interop\Unix\Interop.OpenFlags.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Fcntl.cs"
Link="Common\Interop\Unix\System.Native\Interop.Fcntl.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.SNPrintF.cs"
Link="Common\Interop\Unix\Interop.SNPrintF.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetEUid.cs"
Link="Common\Interop\Unix\Interop.GetEUid.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetPwUid.cs"
Link="Common\Interop\Unix\Interop.GetPwUid.cs" />
</ItemGroup>
</Project>
105 changes: 105 additions & 0 deletions src/libraries/System.Console/tests/TermInfo.Unix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Xunit;

[SkipOnPlatform(TestPlatforms.Android | TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Android, Browser, iOS, MacCatalyst, or tvOS.")]
public class TermInfoTests
{
[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)] // Tests TermInfo
public void VerifyInstalledTermInfosParse()
{
bool foundAtLeastOne = false;

foreach (string location in TermInfo.DatabaseFactory.s_terminfoLocations)
{
if (!Directory.Exists(location))
continue;

foreach (string term in Directory.EnumerateFiles(location, "*", SearchOption.AllDirectories))
{
if (term.ToUpper().Contains("README")) continue;
foundAtLeastOne = true;

TerminalFormatStrings info = new(TermInfo.DatabaseFactory.ReadDatabase(Path.GetFileName(term)));

if (!string.IsNullOrEmpty(info.Foreground))
{
Assert.NotEmpty(TermInfo.ParameterizedStrings.Evaluate(info.Foreground, 0 /* irrelevant, just an integer to put into the formatting*/));
}

if (!string.IsNullOrEmpty(info.Background))
{
Assert.NotEmpty(TermInfo.ParameterizedStrings.Evaluate(info.Background, 0 /* irrelevant, just an integer to put into the formatting*/));
}
}
}

Assert.True(foundAtLeastOne, "Didn't find any terminfo files");
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)] // Tests TermInfo
public void VerifyTermInfoSupportsNewAndLegacyNcurses()
{
Assert.NotNull(TermInfo.DatabaseFactory.ReadDatabase("xterm", "ncursesFormats")); // This will throw InvalidOperationException in case we don't support the legacy format
Assert.NotNull(TermInfo.DatabaseFactory.ReadDatabase("screen-256color", "ncursesFormats")); // This will throw InvalidOperationException if we can't parse the new format
}

[Theory]
[PlatformSpecific(TestPlatforms.AnyUnix)] // Tests TermInfo
[InlineData("xterm-256color", "\u001B\u005B\u00330m", "\u001B\u005B\u00340m", 0)]
[InlineData("xterm-256color", "\u001B\u005B\u00331m", "\u001B\u005B\u00341m", 1)]
[InlineData("xterm-256color", "\u001B\u005B90m", "\u001B\u005B100m", 8)]
[InlineData("screen", "\u001B\u005B\u00330m", "\u001B\u005B\u00340m", 0)]
[InlineData("screen", "\u001B\u005B\u00332m", "\u001B\u005B\u00342m", 2)]
[InlineData("screen", "\u001B\u005B\u00339m", "\u001B\u005B\u00349m", 9)]
[InlineData("Eterm", "\u001B\u005B\u00330m", "\u001B\u005B\u00340m", 0)]
[InlineData("Eterm", "\u001B\u005B\u00333m", "\u001B\u005B\u00343m", 3)]
[InlineData("Eterm", "\u001B\u005B\u003310m", "\u001B\u005B\u003410m", 10)]
[InlineData("wsvt25", "\u001B\u005B\u00330m", "\u001B\u005B\u00340m", 0)]
[InlineData("wsvt25", "\u001B\u005B\u00334m", "\u001B\u005B\u00344m", 4)]
[InlineData("wsvt25", "\u001B\u005B\u003311m", "\u001B\u005B\u003411m", 11)]
[InlineData("mach-color", "\u001B\u005B\u00330m", "\u001B\u005B\u00340m", 0)]
[InlineData("mach-color", "\u001B\u005B\u00335m", "\u001B\u005B\u00345m", 5)]
[InlineData("mach-color", "\u001B\u005B\u003312m", "\u001B\u005B\u003412m", 12)]
public void TermInfoVerification(string termToTest, string expectedForeground, string expectedBackground, int colorValue)
{
TermInfo.Database db = TermInfo.DatabaseFactory.ReadDatabase(termToTest);
if (db != null)
{
TerminalFormatStrings info = new(db);
Assert.Equal(expectedForeground, TermInfo.ParameterizedStrings.Evaluate(info.Foreground, colorValue));
Assert.Equal(expectedBackground, TermInfo.ParameterizedStrings.Evaluate(info.Background, colorValue));
Assert.InRange(info.MaxColors, 1, int.MaxValue);
}
}

[Fact]
[PlatformSpecific(TestPlatforms.OSX)] // The file being tested is available by default only on OSX
public void EmuTermInfoDoesntBreakParser()
{
// This file (available by default on OS X) is called out specifically since it contains a format where it has %i
// but only one variable instead of two. Make sure we don't break in this case
TermInfoVerification("emu", "\u001Br1;", "\u001Bs1;", 0);
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)] // Tests TermInfo
public void TryingToLoadTermThatDoesNotExistDoesNotThrow()
{
const string NonexistentTerm = "foobar____";
TermInfo.Database db = TermInfo.DatabaseFactory.ReadDatabase(NonexistentTerm);
TerminalFormatStrings info = new(db);
Assert.Null(db);
Assert.Null(info.Background);
Assert.Null(info.Foreground);
Assert.Equal(0, info.MaxColors);
Assert.Null(info.Reset);
}
}
Loading