Pour mener des recherches sur le fonctionnement des programmes et des OS, il existe de nombreux outils différents. Machines virtuelles, IDE, blocs-notes intelligents, IDA, radare, éditeurs hexadécimaux, éditeurs pe et même plus d'une centaine d'utilitaires Sysinternals - tout cela est fait pour faciliter de nombreuses opérations de routine. Mais parfois, il arrive un moment où vous vous rendez compte que parmi toute cette diversité, il vous manque un petit utilitaire qui fera simplement un travail banal et simple. Vous pouvez écrire des scripts en python ou Powershell sur le genou, mais souvent vous ne pouvez pas regarder de tels métiers sans larmes et les partager avec vos collègues.
Récemment, cette situation m'est revenue. Et j'ai décidé qu'il était temps de prendre et d'écrire un utilitaire soigné. Je parlerai de l'utilitaire dans l'un des prochains articles, mais je parlerai maintenant de l'un des problèmes pendant le développement.
L'erreur se manifeste comme suit: si vous
insérez plusieurs lignes de texte dans le contrôle TextBox standard, les appels à la fonction
GetLineText () à partir d'un certain index
renverront des lignes incorrectes.
La mauvaise chose est que même si les lignes
proviendront du texte installé, mais seront situées plus loin, en fait
GetLineText () sautera simplement certaines lignes. L'erreur apparaît avec un très grand nombre de lignes. Je l'ai donc rencontrée - j'ai essayé d'afficher 25 mégaoctets de texte dans une TextBox. Travailler avec les dernières lignes a révélé un effet inattendu.
Google
suggère que l'erreur existe depuis 2011 et Microsoft n'est pas pressé de réparer quelque chose.
Exemple
Il n'y a pas d'exigences particulières pour la version .NET. Nous créons un projet WPF standard et remplissons les fichiers comme ceci:
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 (ignorer l'utilisation de et l'espace de noms)
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(); } }
L'application se compose d'un TextBox et de deux boutons. Cliquez d'abord sur "Fire 1!" (remplissez la TextBox avec des nombres), puis "Fire 2!" (il demandera les lignes par numéros et les imprimera).
Résultat attendu:
req: 150, obtenez: 150
req: 300, obtenez: 300
req: 450, obtenez: 450
req: 600, obtenez: 600
req: 750, obtenez: 750
req: 900, obtenez: 900
req: 15000, obtenez: 15000
req: 30000, obtenez: 30000
req: 45000, obtenez: 45000
req: 60000, obtenez: 60000
req: 75000, obtenez: 75000
req: 90000, obtenez: 90000
Réalité:

On peut voir que pour les indices inférieurs à 1 000 - tout va bien, mais pour les 15 000 gros - des décalages ont commencé. Et plus, plus.
Explorez le bug
Nous découvrons la partie du résolveur qui est responsable de l'affichage du code source .NET et de la classe spéciale «Expander d'opportunités et le dépassement des restrictions basées sur la réflexion».
Extension et limiteur de capacité de réflexion 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); } }
Empiriquement, nous établissons que dans un exemple spécifique, le problème commence dans la région de la ligne 8510. Si vous demandez
txt.GetLineText (8510) , «8510» renvoie. Pour 8511 - 8511, et pour 8512 - du coup, 8513.
Nous regardons l'implémentation de
GetLineText () sur un TextBox:

Nous sautons les vérifications dans les premières lignes et voyons l'appel
GetStartPositionOfLine () . Il semble que le problème devrait être dans cette fonction, car pour la mauvaise ligne, la mauvaise position du début de la ligne devrait revenir.
Nous appelons notre code:
var o00 = txt.InvokeMethod("GetStartPositionOfLine", 8510); var o01 = txt.InvokeMethod("GetStartPositionOfLine", 8511); var o02 = txt.InvokeMethod("GetStartPositionOfLine", 8512);
Et la vérité - le décalage du premier objet (le début de la 8510e ligne) est indiqué comme 49950 caractères, pour le deuxième objet - 49956 et le troisième - 49968. Entre les deux 6 premiers caractères et entre les 12 suivants. Désordre - c'est la ligne manquante.
Allez dans
GetStartPositionOfLine () :

Encore une fois, nous sautons les vérifications de démarrage et examinons les actions réelles. Tout d'abord, le point est calculé, ce qui devrait aller à la ligne avec le numéro
lineIndex . La hauteur de toutes les lignes est prise et la moitié de la hauteur de la ligne est ajoutée afin d'atteindre son centre.
Nous ne regardons pas
this.VerticalOffset et
this.HorizontalOffset - ce sont des zéros.
Nous considérons dans notre 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;
Les valeurs sont raisonnables, corrélées à la logique, tout est en ordre. Nous allons plus loin dans le code
GetStartPositionOfLine () - nous sommes intéressés par la ligne significative suivante (la première à l'intérieur de la condition), qui ressemble à un crocodile et se termine par un appel à
GetTextPositionFromPoint () .
Nous ouvrons les défis et les tirons à travers la réflexion. Veuillez noter que certaines interfaces ne sont pas disponibles pour nous en raison de restrictions de visibilité, nous devons donc nous y référer en utilisant la même réflexion.
var renderScope = (txt.GetFieldValue<FrameworkElement>("_renderScope") as IServiceProvider);
Les objets résultants affichent tous les mêmes décalages - 49950, 49956, 49568. Allez plus loin dans l'
implémentation GetTextPositionFromPoint () à l'intérieur de TextBoxView.

Dans,
GetLineIndexFromPoint () semble prometteur. Appelez votre 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);
Nous obtenons 8510, 8511 et 8513 - bingo! Vers la mise en œuvre:

Même à l'œil nu, il est clair qu'il s'agit d'une recherche binaire.
_lineMetrics - une liste de caractéristiques de chaîne (début, longueur, largeur de bordure). Je frotte joyeusement mes stylos - je pensais que comme cela arrive souvent, ils ont oublié de coller
+1 quelque part ou de mettre
> au lieu de
> = . Copiez la fonction dans le code et déboguez-la. En raison de la nature fermée des types
_lineMetrics ,
nous le
retirons par le biais de réflexions,
_lineHeight nous l'avons obtenu plus tôt. 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; }
Nous n'arrivons pas au débogage. o30, o31 et o32 sont respectivement 8510, 8511 et 8512. Tels qu'ils devraient être! Mais o20, o21 et o22 ne sont pas d'accord avec eux. Comment ça? Nous n'avons presque pas changé le code. Presque? Et voici la perspicacité.
var lh = textView.GetFieldValue<double>("_lineHeight");

C'est la raison - la différence est de
0,0009375 . De plus, si nous estimons l'accumulation d'erreurs - nous multiplions par 8511, nous obtenons 7,9790625. C'est à peu près la moitié de lineHeight, et par conséquent, lors du calcul des coordonnées, le point vole en dehors de la ligne souhaitée et tombe sur la suivante. La même variable (dans le sens) a été calculée de deux manières différentes et, soudainement, ne correspondait pas.
Sur ce, j'ai décidé d'arrêter. Il est possible de vraiment comprendre pourquoi la hauteur de la colonne s'est avérée différente, mais je ne vois pas grand-chose. Il est douteux que Microsoft corrige cela, nous examinons donc les béquilles pour contourner ce problème. Reflection-béquille - placez la
_lineHeigh correcte
dans l'un ou l'autre endroit. Cela semble stupide, probablement lent et probablement peu fiable. Ou vous pouvez conserver votre propre ensemble de lignes, parallèlement à la TextBox, et en prendre des lignes, car l'obtention du numéro de ligne à la position du curseur fonctionne correctement.
Conclusion
De programmeurs novices, vous pouvez souvent entendre quelque chose sur les bogues du compilateur ou des composants standard. En réalité, ils ne sont pas si communs, mais personne n'est à l'abri d'eux. N'ayez pas peur de regarder à l'intérieur de l'outil dont vous avez besoin - c'est passionnant et intéressant.
Écrivez un bon code!
Autres articles de blog
→
Apprentissage automatique à la sécurité offensiveAucune voiture ne peut me remplacer. Muhaha ha. Je l'espère.
→
Où insérer un guillemet dans IPv6Les gars savent où et quoi mettre pour que ça se passe bien. Après de tels mots, ils vont certainement me remplacer par un robot. Mer! UHF!