diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj index ea763aa3a46310..c60368c33b8b50 100644 --- a/src/libraries/System.Console/src/System.Console.csproj +++ b/src/libraries/System.Console/src/System.Console.csproj @@ -194,8 +194,6 @@ Link="Common\Interop\Unix\Interop.GetControlCharacters.cs" /> - Reads data from the file descriptor into the buffer. /// The file descriptor. /// The buffer to read into. - /// The number of bytes read, or a negative value if there's an error. - internal static unsafe int Read(SafeFileHandle fd, Span buffer) + /// The number of bytes read, or an exception if there's an error. + private static unsafe int Read(SafeFileHandle fd, Span buffer) { fixed (byte* bufPtr = buffer) { diff --git a/src/libraries/System.Console/src/System/TermInfo.DatabaseFactory.cs b/src/libraries/System.Console/src/System/TermInfo.DatabaseFactory.cs index 844475e4851d59..0db908b2729bbd 100644 --- a/src/libraries/System.Console/src/System/TermInfo.DatabaseFactory.cs +++ b/src/libraries/System.Console/src/System/TermInfo.DatabaseFactory.cs @@ -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. /// - private static readonly string[] _terminfoLocations = new string[] { + internal static readonly string[] s_terminfoLocations = { "/etc/terminfo", "/lib/terminfo", "/usr/share/terminfo", @@ -34,7 +34,7 @@ internal sealed class DatabaseFactory /// Read the database for the specified terminal. /// The identifier for the terminal. /// The database, or null if it could not be found. - private static Database? ReadDatabase(string term) + internal static Database? ReadDatabase(string term) { // This follows the same search order as prescribed by ncurses. Database? db; @@ -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) { @@ -88,7 +88,7 @@ private static bool TryOpen(string filePath, [NotNullWhen(true)] out SafeFileHan /// The identifier for the terminal. /// The path to the directory containing terminfo database files. /// The database, or null if it could not be found. - private static Database? ReadDatabase(string? term, string? directoryPath) + internal static Database? ReadDatabase(string? term, string? directoryPath) { if (string.IsNullOrEmpty(term) || string.IsNullOrEmpty(directoryPath)) { @@ -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) @@ -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(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); diff --git a/src/libraries/System.Console/tests/System.Console.Tests.csproj b/src/libraries/System.Console/tests/System.Console.Tests.csproj index 311abe1aaf8735..f4b451d30e147b 100644 --- a/src/libraries/System.Console/tests/System.Console.Tests.csproj +++ b/src/libraries/System.Console/tests/System.Console.Tests.csproj @@ -22,7 +22,6 @@ - + + + + + + + + + + + + + diff --git a/src/libraries/System.Console/tests/TermInfo.Unix.cs b/src/libraries/System.Console/tests/TermInfo.Unix.cs new file mode 100644 index 00000000000000..03da592c759e6c --- /dev/null +++ b/src/libraries/System.Console/tests/TermInfo.Unix.cs @@ -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); + } +} diff --git a/src/libraries/System.Console/tests/TermInfo.cs b/src/libraries/System.Console/tests/TermInfo.cs deleted file mode 100644 index 60336b23a0646f..00000000000000 --- a/src/libraries/System.Console/tests/TermInfo.cs +++ /dev/null @@ -1,208 +0,0 @@ -// 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 TermInfo -{ - // Names of internal members accessed via reflection - private const string TerminfoType = "System.TermInfo"; - private const string TerminfoDatabaseFactoryType = TerminfoType + "+DatabaseFactory"; - private const string ParameterizedStringsType = TerminfoType + "+ParameterizedStrings"; - private const string FormatParamType = ParameterizedStringsType + "+FormatParam"; - private const string TerminalFormatStringsType = "System.TerminalFormatStrings"; - private const string ReadDatabaseMethod = "ReadDatabase"; - private const string EvaluateMethod = "Evaluate"; - private const string ForegroundFormatField = "Foreground"; - private const string BackgroundFormatField = "Background"; - private const string ResetFormatField = "Reset"; - private const string MaxColorsField = "MaxColors"; - private const string TerminfoLocationsField = "_terminfoLocations"; - - [Fact] - [PlatformSpecific(TestPlatforms.AnyUnix)] // Tests TermInfo - public void VerifyInstalledTermInfosParse() - { - bool foundAtLeastOne = false; - - string[] locations = GetFieldValueOnObject(TerminfoLocationsField, null, typeof(Console).GetTypeInfo().Assembly.GetType(TerminfoDatabaseFactoryType)); - foreach (string location in locations) - { - if (!Directory.Exists(location)) - continue; - - foreach (string term in Directory.EnumerateFiles(location, "*", SearchOption.AllDirectories)) - { - if (term.ToUpper().Contains("README")) continue; - foundAtLeastOne = true; - - object info = CreateTermColorInfo(ReadTermInfoDatabase(Path.GetFileName(term))); - - if (!string.IsNullOrEmpty(GetForegroundFormat(info))) - { - Assert.NotEmpty(EvaluateParameterizedStrings(GetForegroundFormat(info), 0 /* irrelevant, just an integer to put into the formatting*/)); - } - - if (!string.IsNullOrEmpty(GetBackgroundFormat(info))) - { - Assert.NotEmpty(EvaluateParameterizedStrings(GetBackgroundFormat(info), 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() - { - MethodInfo readDbMethod = typeof(Console).GetTypeInfo().Assembly.GetType(TerminfoDatabaseFactoryType).GetTypeInfo().GetDeclaredMethods(ReadDatabaseMethod).Where(m => m.GetParameters().Count() == 2).Single(); - readDbMethod.Invoke(null, new object[] { "xterm", "ncursesFormats" }); // This will throw InvalidOperationException in case we don't support the legacy format - readDbMethod.Invoke(null, new object[] { "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) - { - object db = ReadTermInfoDatabase(termToTest); - if (db != null) - { - object info = CreateTermColorInfo(db); - Assert.Equal(expectedForeground, EvaluateParameterizedStrings(GetForegroundFormat(info), colorValue)); - Assert.Equal(expectedBackground, EvaluateParameterizedStrings(GetBackgroundFormat(info), colorValue)); - Assert.InRange(GetMaxColors(info), 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____"; - object db = ReadTermInfoDatabase(NonexistentTerm); - object info = CreateTermColorInfo(db); - Assert.Null(db); - Assert.Null(GetBackgroundFormat(info)); - Assert.Null(GetForegroundFormat(info)); - Assert.Equal(0, GetMaxColors(info)); - Assert.Null(GetResetFormat(info)); - } - - private object ReadTermInfoDatabase(string term) - { - MethodInfo readDbMethod = typeof(Console).GetTypeInfo().Assembly.GetType(TerminfoDatabaseFactoryType).GetTypeInfo().GetDeclaredMethods(ReadDatabaseMethod).Where(m => m.GetParameters().Count() == 1).Single(); - return readDbMethod.Invoke(null, new object[] { term }); - } - - private object CreateTermColorInfo(object db) - { - return typeof(Console).GetTypeInfo().Assembly.GetType(TerminalFormatStringsType).GetTypeInfo().DeclaredConstructors - .Where(c => c.GetParameters().Count() == 1).Single().Invoke(new object[] { db }); - } - - private string GetForegroundFormat(object colorInfo) - { - return GetFieldValueOnObject(ForegroundFormatField, colorInfo, typeof(Console).GetTypeInfo().Assembly.GetType(TerminalFormatStringsType)); - } - - private string GetBackgroundFormat(object colorInfo) - { - return GetFieldValueOnObject(BackgroundFormatField, colorInfo, typeof(Console).GetTypeInfo().Assembly.GetType(TerminalFormatStringsType)); - } - - private int GetMaxColors(object colorInfo) - { - return GetFieldValueOnObject(MaxColorsField, colorInfo, typeof(Console).GetTypeInfo().Assembly.GetType(TerminalFormatStringsType)); - } - - private string GetResetFormat(object colorInfo) - { - return GetFieldValueOnObject(ResetFormatField, colorInfo, typeof(Console).GetTypeInfo().Assembly.GetType(TerminalFormatStringsType)); - } - - private T GetFieldValueOnObject(string name, object instance, Type baseType) - { - return (T)baseType.GetTypeInfo().GetDeclaredField(name).GetValue(instance); - } - - private object CreateFormatParam(object o) - { - Assert.True((o.GetType() == typeof(int)) || (o.GetType() == typeof(string))); - - TypeInfo ti = typeof(Console).GetTypeInfo().Assembly.GetType(FormatParamType).GetTypeInfo(); - ConstructorInfo ci = null; - - foreach (ConstructorInfo c in ti.DeclaredConstructors) - { - Type paramType = c.GetParameters().ElementAt(0).ParameterType; - if ((paramType == typeof(string)) && (o.GetType() == typeof(string))) - { - ci = c; - break; - } - else if ((paramType == typeof(int)) && (o.GetType() == typeof(int))) - { - ci = c; - break; - } - } - - Assert.True(ci != null); - return ci.Invoke(new object[] { o }); - } - - private string EvaluateParameterizedStrings(string format, params object[] parameters) - { - Type formatArrayType = typeof(Console).GetTypeInfo().Assembly.GetType(FormatParamType).MakeArrayType(); - MethodInfo mi = typeof(Console).GetTypeInfo().Assembly.GetType(ParameterizedStringsType).GetTypeInfo() - .GetDeclaredMethods(EvaluateMethod).First(m => m.GetParameters()[1].ParameterType.IsArray); - - // Create individual FormatParams - object[] stringParams = new object[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) - stringParams[i] = CreateFormatParam(parameters[i]); - - // Create the array of format params and then put the individual params in their location - Array typeArray = (Array)Activator.CreateInstance(formatArrayType, new object[] { stringParams.Length }); - for (int i = 0; i < parameters.Length; i++) - typeArray.SetValue(stringParams[i], i); - - // Setup the params to evaluate - object[] evalParams = new object[2]; - evalParams[0] = format; - evalParams[1] = typeArray; - - return (string)mi.Invoke(null, evalParams); - } -} \ No newline at end of file