📜 ⬆️ ⬇️

Bug when working TextBox.GetLineText in .NET WPF

There are many different tools for conducting research on the operation of programs and operating systems. Virtual machines, IDE, smart notebooks, IDA, radare, hex editors, pe editors, and even more than a hundred Sysinternals utilities are all done to facilitate many routine operations. But sometimes a moment comes when you realize that among all this variety you lack a small utility that just does the banal and simple work. You can write scripts on python or Powershell on your knee, but often you can’t look at such crafts without tears and share it with your colleagues.

Recently, this situation has come to me again. And I decided it was time to just take and write a neat utility. I will tell you about the utility itself in one of the upcoming articles, but I’ll tell you about one of the problems during development.

The error manifests itself in the following way: if in a WPF application, sticking many lines of text into the standard TextBox control, then calls to the GetLineText () function starting from a certain index will return incorrect lines.
')

The flaw is that even though the lines will be from the set text, but located further, in fact, GetLineText () will simply skip some lines. The error manifests itself with a very large number of lines. So I met her - I tried to display 25 megabytes of text in TextBox. Working with the latest lines revealed an unexpected effect.

Google suggests that the error exists since 2011 and Microsoft is not particularly in a hurry to fix something.

Example


There are no requirements for the .NET version. Create a standard WPF project and populate the files like this:
MainWindow.xaml

<Window x:Class="wpf_textbox.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:wpf_textbox" mc:Ignorable="d" Title="WTF, WPF?" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="20"/> <RowDefinition Height="20"/> </Grid.RowDefinitions> <TextBox Grid.Row="0" Margin="5" Name="txt" AcceptsReturn="True" AcceptsTab="True" /> <Button Grid.Row="1" Content="Fire 1!" Click="btn_OnClick" /> <Button Grid.Row="2" Content="Fire 2!" Click="btn2_OnClick" /> </Grid> </Window> 

MainWindow.cs (skipping using and namespace)

 public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void btn_OnClick(object sender, RoutedEventArgs e) { var sb = new StringBuilder(); for (int i = 0; i < 90009; i++) sb.AppendLine($"{i}"); txt.Text = sb.ToString(); } private void btn2_OnClick(object sender, RoutedEventArgs e) { var sb = new StringBuilder(); for (var i = 1; i < 7; i++) sb.AppendLine("req: " + 150 * i + ", get: " + txt.GetLineText(150 * i).Trim()); for (var i = 1; i < 7; i++) sb.AppendLine("req: " + 15000 * i + ", get: " + txt.GetLineText(15000 * i).Trim()); txt.Text = sb.ToString(); } } 

The application consists of a TextBox and two buttons. First, click “Fire 1!” (Fill in the TextBox with numbers), then “Fire 2!” (Ask for lines by numbers and output).

Expected Result:
req: 150, get: 150
req: 300, get: 300
req: 450, get: 450
req: 600, get: 600
req: 750, get: 750
req: 900, get: 900
req: 15000, get: 15000
req: 30000, get: 30000
req: 45000, get: 45000
req: 60000, get: 60000
req: 75000, get: 75000
req: 90000, get: 90000

Reality:



It can be seen that for indexes less than 1000 - everything is fine, and for large 15,000 - shifts have begun. And the further, the more.

Investigate the bug


We uncover that part of the resharper, which is responsible for viewing the .NET source code and the special class “Extender of capabilities and override of restrictions based on Reflection”

Extender of opportunities and override of restrictions on the basis of Reflection
 public static class ReflectionExtensions { public static T GetFieldValue<T>(this object obj, string name) { var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var field = obj.GetType().GetField(name, bindingFlags); if (field == null) field = obj.GetType().BaseType.GetField(name, bindingFlags); return (T)field?.GetValue(obj); } public static object InvokeMethod(this object obj, string methodName, params object[] methodParams) { var methodParamTypes = methodParams?.Select(p => p.GetType()).ToArray() ?? new Type[] { }; var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; MethodInfo method = null; var type = obj.GetType(); while (method == null && type != null) { method = type.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null); var intfs = type.GetInterfaces(); if (method != null) break; foreach (var intf in intfs) { method = intf.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null); if (method != null) break; } type = type.BaseType; } return method?.Invoke(obj, methodParams); } } 


It is established experimentally that in a given example, the problem begins in the region of 8510 lines. If you request txt.GetLineText (8510) , then “8510” will return. For 8511 - 8511, and for 8512 - suddenly, 8513.

We look at the TextBox GetLineText () implementation:


We skip the checks in the first lines and see the call to GetStartPositionOfLine () . It seems that the problem should be in this function, because for the wrong line the wrong position of the beginning of the line should return.

Call in your code:

 var o00 = txt.InvokeMethod("GetStartPositionOfLine", 8510); var o01 = txt.InvokeMethod("GetStartPositionOfLine", 8511); var o02 = txt.InvokeMethod("GetStartPositionOfLine", 8512); 

And the truth is that the offset of the first object (the beginning of the 8510th line) is indicated as 49950 characters, for the second object - 49956, and the third - 49968. Between the first two 6 characters, and between the next 12. Disorder - this is the missing line.

Go inside GetStartPositionOfLine () :



Again we skip the initial checks and look at the real actions. First, the point is calculated, which should fall on the line number lineIndex . The height of all lines is taken and a half of the line height is added - in order to get to its center. We do not look at this.VerticalOffset and this.HorizontalOffset - they are by zeros.

We consider in our code:

 var lineHeight = (double) txt.InvokeMethod("GetLineHeight", null); var y0 = lineHeight * (double)8510 + lineHeight / 2.0 - txt.VerticalOffset; var y1 = lineHeight * (double)8511 + lineHeight / 2.0 - txt.VerticalOffset; var y2 = lineHeight * (double)8512 + lineHeight / 2.0 - txt.VerticalOffset; 

Values ​​are reasonable, correlated with logic, everything is in order. Go ahead with the GetStartPositionOfLine () code - we are interested in the next meaningful line (the first one inside the condition), which is similar to a crocodile and ends with a call to GetTextPositionFromPoint () .

We reveal the challenges and pull them through reflection. Note that some interfaces are not available to us due to the restriction of visibility, so you have to refer to them using the same Reflection.

 var renderScope = (txt.GetFieldValue<FrameworkElement>("_renderScope") as IServiceProvider); // 7 -   ITextView var textView = renderScope.GetService(renderScope.GetType().GetInterfaces()[7]); var o10 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y0), true); var o11 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y1), true); var o12 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y2), true); 

The resulting objects show all the same offsets - 49950, 49956, 49568. Going deeper into the implementation of GetTextPositionFromPoint () inside TextBoxView.


In, GetLineIndexFromPoint () looks promising. Call in your code.

 var o20 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y0), true); var o21 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y1), true); var o22 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y2), true); 

We get 8510, 8511 and 8513 - bingo! To implementation:



Even with the naked eye it is clear that this is a binary search. _lineMetrics - list of characteristics of lines (beginning, length, width of the border). I happily rub my hands - I thought that, as is often the case, somewhere we forgot to stick a +1 or set > instead of > = . Copy the function into the code and debug it. Because of the closeness of the _lineMetrics types, we pull out through the reflections, we already got _lineHeight earlier. Total:

 var lm = textView.GetFieldValue<object>("_lineMetrics"); var c = (int)lm.InvokeMethod("get_Count"); var lineMetrics = new List<Tuple<int,int,int,double>>(); for (var i = 0; i < c; i++) { var arr_o = lm.InvokeMethod("get_Item", i); var contLength = arr_o.GetFieldValue<int>("_contentLength"); var length = arr_o.GetFieldValue<int>("_length"); var offset = arr_o.GetFieldValue<int>("_offset"); var width = arr_o.GetFieldValue<double>("_width"); lineMetrics.Add(new Tuple<int, int, int, double>(contLength, length, offset, width)); } var o30 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y0), true); var o31 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y1), true); var o32 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y2), true); /*<...>*/ private int GetLineIndexFromPoint(List<Tuple<int, int, int, double>> lm, double _lineHeight, Point point, bool snapToText) { if (point.Y < 0.0) return !snapToText ? -1 : 0; if (point.Y >= _lineHeight * (double)lm.Count) { if (!snapToText) return -1; return lm.Count - 1; } int index = -1; int num1 = 0; int num2 = lm.Count; while (num1 < num2) { index = num1 + (num2 - num1) / 2; var lineMetric = lm[index]; double num3 = _lineHeight * (double)index; if (point.Y < num3) num2 = index; else if (point.Y >= num3 + _lineHeight) { num1 = index + 1; } else { if (!snapToText && (point.X < 0.0 || point.X >= lineMetric.Item4)) { index = -1; break; } break; } } if (num1 >= num2) return -1; return index; } 

We do not get to debug. o30, o31, and o32 are 8510, 8511, and 8512, respectively. Such as they should be! But o20, o21 and o22 do not agree with them. How so? We almost did not change the code. Nearly? And here comes the insight.

 var lh = textView.GetFieldValue<double>("_lineHeight"); 



Here it is the reason - the difference is 0.0009375 . And if we estimate the accumulation of the error - we multiply by 8511, then we get 7.9790625. This is just about half of the lineHeight, and therefore, when calculating the coordinates, the point flies out beyond the limits of the desired line and falls on the next. The same variable (meaning) was calculated in two different ways and, suddenly, did not match.

On this I decided to stop. It is possible to really get to the bottom of why the height of the column turned out to be different, but I do not see much point. It is doubtful that Microsoft will fix this, so we are looking at crutches for workarounds. Reflection Crutch - Install the correct _lineHeigh in either one or the other. It sounds dumb, probably slowly and most likely unreliable. Or you can maintain your own set of lines, parallel to TextBox and take lines from it, the benefit of getting the line number for the cursor position works correctly.

Conclusion


From novice programmers, you can often hear something about errors in the compiler or standard components. In reality, they are not as common, but none of them are insured. Do not be afraid to look inside the tool that you need - it's fun and interesting.

Write a good code!

Other blog articles


→ Machine Training in Offensive Security
No car will replace me. Muhaha-ha. I hope.

→ Where to insert quotation mark in IPv6
The guys know where and what to shove in order to become good. After such words, they will replace me with a robot, for sure. Wed! UHF!

Source: https://habr.com/ru/post/420361/


All Articles