From 84641bff03ae5766dafc9e90e3593d93232a82bd Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:54:40 +0000 Subject: [PATCH] GP-5689 Add ByteViewer 'Chars' column Convert the Ascii format to Chars, which supports decoding bytes using any available charset. Reworked the memory alignment option of the ByteViewer to allow the user to bump the displayed bytes left/right using an action. Add option to control how widely spaced the chars are displayed, to allow for wider non-Latin characters to be displayed correctly. Refactor the CharsetInfo class to better support the information needed by the new chars column. Fixed HexLongLong format to not mix BE / LE longs when constructing its final 16 byte value. Fixed handling of ByteViewer's background color to use its theme value. --- .../DebuggerMemoryByteViewerComponent.java | 10 +- .../gui/memory/DebuggerMemoryBytesPanel.java | 6 +- Ghidra/Features/Base/certification.manifest | 2 + .../Features/Base/data/base.theme.properties | 2 +- .../Base/src/main/help/help/TOC_Source.xml | 1 + .../help/help/topics/Charsets/Charsets.htm | 96 + .../Charsets/images/CharsetPickerDialog.png | Bin 0 -> 38486 bytes .../core/strings/EncodedStringsDialog.java | 15 +- .../core/strings/EncodedStringsPlugin.java | 4 +- .../util/charset/picker/CharsetInfoPanel.java | 161 + .../charset/picker/CharsetPickerDialog.java | 78 + .../charset/picker/CharsetPickerPanel.java | 103 + .../charset/picker/CharsetTableModel.java | 125 + .../util/charset/picker/CharsetTableRow.java | 20 + .../ByteViewerPlugin/The_Byte_Viewer.htm | 549 +-- .../ByteViewerPlugin/images/ByteViewer.png | Bin 20692 -> 25034 bytes .../images/ByteViewerExample.png | Bin 20178 -> 24838 bytes .../images/ByteViewerOptionsDialog.png | Bin 14385 -> 13609 bytes .../images/ByteViewerResults.png | Bin 20224 -> 24985 bytes .../app/plugin/core/byteviewer/ByteField.java | 6 +- .../byteviewer/ByteViewerActionContext.java | 22 +- .../byteviewer/ByteViewerBGColorModel.java | 58 + .../ByteViewerClipboardProvider.java | 8 +- .../core/byteviewer/ByteViewerComponent.java | 356 +- .../byteviewer/ByteViewerComponentNamer.java | 24 + .../ByteViewerComponentProvider.java | 319 +- .../byteviewer/ByteViewerConfigOptions.java | 162 + .../byteviewer/ByteViewerHighlighter.java | 11 +- .../byteviewer/ByteViewerIndexedView.java | 9 +- .../byteviewer/ByteViewerLayoutModel.java | 52 +- .../byteviewer/ByteViewerOptionsDialog.java | 506 +-- .../core/byteviewer/ByteViewerPanel.java | 255 +- .../core/byteviewer/EmptyByteBlockSet.java | 11 +- .../plugin/core/byteviewer/FieldFactory.java | 69 +- .../plugin/core/byteviewer/FileByteBlock.java | 34 +- .../core/byteviewer/IndexFieldFactory.java | 33 +- .../byteviewer/InteractivePanelManager.java | 17 +- .../core/byteviewer/MemoryByteBlock.java | 43 +- .../plugin/core/byteviewer/OptionsAction.java | 47 - .../core/byteviewer/ProgramByteBlockSet.java | 8 +- .../ProgramByteViewerComponentProvider.java | 58 +- .../core/byteviewer/ToggleEditAction.java | 47 - .../core/format/AddressFormatModel.java | 57 +- .../plugin/core/format/AsciiFormatModel.java | 197 - .../plugin/core/format/BinaryFormatModel.java | 66 +- .../app/plugin/core/format/ByteBlock.java | 11 + .../app/plugin/core/format/ByteBlockSet.java | 8 + .../core/format/CharacterFormatModel.java | 236 ++ .../format/CursorWidthDataFormatModel.java | 26 + .../plugin/core/format/DataFormatModel.java | 97 +- .../core/format/DisassembledFormatModel.java | 71 +- .../plugin/core/format/HexFormatModel.java | 268 +- .../core/format/HexIntegerFormatModel.java | 6 +- .../core/format/HexLongFormatModel.java | 6 +- .../core/format/HexLongLongFormatModel.java | 20 +- .../core/format/HexShortFormatModel.java | 8 +- .../core/format/HexValueFormatModel.java | 62 +- .../core/format/IntegerFormatModel.java | 98 +- .../core/format/MutableDataFormatModel.java | 42 + .../plugin/core/format/OctalFormatModel.java | 78 +- .../core/format/TooltipDataFormatModel.java | 27 + .../AbstractByteViewerPluginTest.java | 516 +++ .../byteviewer/ByteViewerPlugin1Test.java | 522 +-- .../byteviewer/ByteViewerPlugin2Test.java | 1587 +++----- .../byteviewer/ByteViewerPlugin3Test.java | 136 +- .../ByteViewerPluginCharFormatTest.java | 162 + .../ByteViewerPluginFormatsTest.java | 265 +- .../pdb2/pdbreader/PdbReaderOptions.java | 10 +- .../widgets/spinner/IntegerSpinner.java | 26 +- .../SoftwareModeling/certification.manifest | 2 +- .../SoftwareModeling/data/charset_info.json | 3213 +++++++++++++++++ .../SoftwareModeling/data/charset_info.xml | 19 - .../ghidra/SoftwareModelingInitializer.java | 8 +- .../program/model/data/CharDataType.java | 9 +- .../program/model/data/CharsetInfo.java | 210 -- .../model/data/CharsetSettingsDefinition.java | 4 +- .../model/data/PascalUnicodeDataType.java | 4 +- .../model/data/StringDataInstance.java | 24 +- .../model/data/StringRenderParser.java | 23 +- .../model/data/StringUTF8DataType.java | 4 +- .../data/TerminatedUnicode32DataType.java | 4 +- .../model/data/TerminatedUnicodeDataType.java | 8 +- .../program/model/data/Unicode32DataType.java | 4 +- .../program/model/data/UnicodeDataType.java | 4 +- .../model/data/WideChar16DataType.java | 3 +- .../model/data/WideChar32DataType.java | 7 +- .../program/model/data/WideCharDataType.java | 9 +- .../java/ghidra/util/charset/CharsetInfo.java | 192 + .../util/charset/CharsetInfoManager.java | 375 ++ .../util/charset/CharsetInfoManagerTest.java | 170 + .../ByteViewerPluginScreenShots.java | 58 +- .../help/screenshot/CharsetsScreenShots.java | 37 + 92 files changed, 7992 insertions(+), 4334 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/Charsets/Charsets.htm create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/Charsets/images/CharsetPickerDialog.png create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetInfoPanel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerPanel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableModel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableRow.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerBGColorModel.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentNamer.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerConfigOptions.java delete mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/OptionsAction.java delete mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ToggleEditAction.java delete mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AsciiFormatModel.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CharacterFormatModel.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CursorWidthDataFormatModel.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/MutableDataFormatModel.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/TooltipDataFormatModel.java create mode 100644 Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/AbstractByteViewerPluginTest.java create mode 100644 Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginCharFormatTest.java create mode 100644 Ghidra/Framework/SoftwareModeling/data/charset_info.json delete mode 100644 Ghidra/Framework/SoftwareModeling/data/charset_info.xml delete mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetInfo.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfo.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfoManager.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/util/charset/CharsetInfoManagerTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/CharsetsScreenShots.java diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryByteViewerComponent.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryByteViewerComponent.java index 8bfade0898..f871a79c3f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryByteViewerComponent.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryByteViewerComponent.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,7 +16,6 @@ package ghidra.app.plugin.core.debug.gui.memory; import java.awt.Color; -import java.awt.FontMetrics; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; @@ -118,9 +117,8 @@ public class DebuggerMemoryByteViewerComponent extends ByteViewerComponent private final List selectionGenerators; public DebuggerMemoryByteViewerComponent(DebuggerMemoryBytesPanel vpanel, - ByteViewerLayoutModel layoutModel, DataFormatModel model, int bytesPerLine, - FontMetrics fm) { - super(vpanel, layoutModel, model, bytesPerLine, fm); + ByteViewerLayoutModel layoutModel, DataFormatModel model, int bytesPerLine) { + super(vpanel, layoutModel, model, bytesPerLine); // TODO: I don't care much for this reverse path this.panel = vpanel; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPanel.java index 947a704760..3ce09ce7fb 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/memory/DebuggerMemoryBytesPanel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -37,6 +37,6 @@ public class DebuggerMemoryBytesPanel extends ByteViewerPanel { @Override protected ByteViewerComponent newByteViewerComponent(DataFormatModel model) { return new DebuggerMemoryByteViewerComponent(this, new ByteViewerLayoutModel(), model, - getBytesPerLine(), getFontMetrics()); + getBytesPerLine()); } } diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 2ac9fdce0d..489a593cd8 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -279,6 +279,8 @@ src/main/help/help/topics/CParserPlugin/images/UseOpenArchives.png||GHIDRA||||EN src/main/help/help/topics/CallTreePlugin/Call_Tree_Plugin.html||GHIDRA||||END| src/main/help/help/topics/CallTreePlugin/images/CallTreeWindow.png||GHIDRA||||END| src/main/help/help/topics/CallTreePlugin/images/depth-input.png||GHIDRA||reviewed||END| +src/main/help/help/topics/Charsets/Charsets.htm||GHIDRA||||END| +src/main/help/help/topics/Charsets/images/CharsetPickerDialog.png||GHIDRA||||END| src/main/help/help/topics/ClearPlugin/Clear.htm||GHIDRA||||END| src/main/help/help/topics/ClearPlugin/images/ClearFlow.png||GHIDRA||||END| src/main/help/help/topics/ClearPlugin/images/ClearWithOptions.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/data/base.theme.properties b/Ghidra/Features/Base/data/base.theme.properties index 0d04c30b02..f77102a815 100644 --- a/Ghidra/Features/Base/data/base.theme.properties +++ b/Ghidra/Features/Base/data/base.theme.properties @@ -207,7 +207,7 @@ font.plugin.terminal.dim.fraktur = font.plugin.terminal.dim font.plugin.terminal.completion.list = dialog-plain-12 font.plugin.window.location = font.monospaced[40] - +font.textarea.astextfield = [laf.font]TextField.font [Dark Defaults] diff --git a/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml b/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml index b7d1d90c6d..47b845e71c 100644 --- a/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/Base/src/main/help/help/TOC_Source.xml @@ -236,6 +236,7 @@ + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Charsets/Charsets.htm b/Ghidra/Features/Base/src/main/help/help/topics/Charsets/Charsets.htm new file mode 100644 index 0000000000..861e45e8ab --- /dev/null +++ b/Ghidra/Features/Base/src/main/help/help/topics/Charsets/Charsets.htm @@ -0,0 +1,96 @@ + + + + + Charsets + + + + + +

Charsets

+ +

Charsets are named mappings between byte sequences and Unicode code points.

+

Common examples: +

    +
  • US-ASCII
  • +
  • UTF-8
  • +
  • UTF-16
  • +
+

+ +

Unicode Code Point

+ +

The term "code point" denotes an integer that represents a character in the Unicode standard. + The first 127 code points map directly to the well-known ASCII characters.

+ +

There are approximately 1.1 million defined code points in the Unicode standard.

+ +

Unicode Scripts

+ +

Unicode code points are grouped into categories called 'scripts'. For example, the Latin + script contains the well-known ABC..XYZ characters, the Common script contains numbers (0-9) and + punctuation, and the Greek script contains characters such as the delta 'Δ' symbol.

+ +

Do not conflate a script (alphabet) with a human language, even though there can be some + correlation.

+ +

The Unicode standard that Java implements has about 160 different scripts.

+ +

Charset Picker

+ +

The charset picker dialog allows the user to browse the available installed charsets + and select one to be used when decoding strings / character data.

+ +
+
+ +
+
+ +

The dialog has a table with the following columns:

+ +

Name

+

Name of the charset. Charsets not in the IANA Charset Registry will have a "x-" prefix

+ +

Description

+

A short description of the charset.

+ +

Fixed Length

+

Flag to indicate that the charset always uses a fixed number of bytes to encode a character.

+ +

Min BPC / Max BPC (Bytes Per Character)

+

The minimum and maximum number of bytes that the charset uses to encoded a single character. + This value may include the length of escape sequences that typically only appear once in a + string.

+ +

Scripts

+

A list of the scripts (alphabets) that the charset can encode/decode characters for. For + example, US-ASCII can only produce Latin characters (along with ubiquitous Common characters), + whereas the UTF-8 charset can encoded characters for every script that the Unicode standard + supports.

+ +

In the Details panel, the Scripts list also can contain a short snippet of example + characters from that script (if your font supports the script in question).

+ +

Charset Details

+

Below the dialog's table is an area that displays the details of the charset, which + in addition to the information displayed in the table contains:

+ +

Aligned Size

+

For charsets where the bytes of encoded characters should be treated as a larger-than-byte + integer type (because they would have been specified as arrays of those integer types), this + field will specify the size of that integer. Currently, only UTF-16 and UTF-32 specify a + value for this setting.

+ +

Related Topics:

+ + +

+ + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Charsets/images/CharsetPickerDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/Charsets/images/CharsetPickerDialog.png new file mode 100644 index 0000000000000000000000000000000000000000..032dc28a3270ee8238f3060ce7812be0a720a7ac GIT binary patch literal 38486 zcmbT8by$_%*6xw+mXdBk8tIVkly2$nlu{`v0cin|FzD{?Zs~66?m83neZP0_v-fwM z>-e86*0a_#o-xL}=WpDTU`2Te6hs0<2nYxiDM?Xf2nfhp2#80=@Q~mqio6!@At2sB zNQnxmxa)1}!Z|-~J3}R1hX^^Q`|=)ozkn1z1QOE{o>c4^yO>JMTqf2h$amNnnefBM zaN_ANexyrE3d^NYWd^YeBW4bZc-%#BzDnlQ)+2k1GPmbNV|LnKS5i{L=GHJ-vDfw# z{9|EZAtyJSulKf6EBJtnR+AjbZIYHJ&=@4I{wXZI=s35U`7ON!Sz zjh{fr9F0O^dM-I;@P(voDca?kAkoSIhe9gy_SKLCGaY}EzY~l6!FqbXSO8XVtAso? zfp{1}cyVfh+t%sX*yZlXx2OCrowlc2;{~c}QZw~CXp+(DdR78Yp$#qwCaw8_ELB&=x0s6 zZysAUU8OdB^%e_+sd4`Gek#i7(3K{tjO}QvK;UvEdR)8Ac5BLFG=g69u`l`X`3Waa z-Rw@y9*&ICh-rVL;c8i2-dL_K%JRpr^DQWlU;VUk31jYxT6!;fyGYM0cJp;>IIM<> zWzL9Y;=FK)9B^FY+PJiqykkgG~V?_X=V4M`5Jx-b~V*NFbUZzWsG%!C7uW{l}S^?+OxW zM(gt7U66kRP0a65cD8h9&W5bJ$&q zg}vkAf=@IozpmR;(5OCp&3SID*jUs;ul6ixjN44WzrwTLkM5XnMP=|;k&)78b`RE2 z0xt8&N-0WAqz?%RvYc;rc&f5Yk>$T9bSjcZnM1pBZoR?C5Kr4)_k82c$KvvdH^<~` z*=5T|_<}Q{&pI#_nUDH;^Gvn#-iL-tdbmpx)m`}Oi*=OT0PibTdMgj!&<1V)!wsyr zy&eO*L?5TI&$3YV`1A27#2K`_m_mlErb!BGHE;5Z^C z{Sp@`VMO32@|R;I$ehIpZuO$Ufs$?VOR086;_?yQXxZ>zg^ZG9hfFj#u)Y*W$~|1~ zOTu+HMb5cVXBYKnyn2P-T~KEJ)!M&`g;AsO#YSUgi`A2(i=FYlZml`g>DkEn#RqsU0lI|Bw$@R@$LHDVZ-=cl?62n7=cyP%!j?F}qzw;EULvvU_LL`7t{y9;_A zKL0xRp|h5V7$x$7WcZ1VGMlyB#N3pH=tsFJ-|Wi6kohxau4q!(hM1*(*VXL2iJgEe z+OZ&H`rzQTNBVH|&{LkJXZK;MGFMa+Lpn2xp%5N$_FSJ^Zf$A_gt;K$1uep+-MMRL zT%YrmW(3vk;i@35Hq_nz%mPO+u8T1O*6gaz zFNnGJ2kPT&_Nq=U_9;xTms5RriP>yt^nG^`Py{y0yy-3V?JjUG(Oh>N_Rzn{r{Z|g z?Vk^cSdHJYoA0CJJYVZ!QhIJb_@%SZz+@m;)X?YPv#9%79mE}lHCBLa-EN)E*JESn zX05k>YDnzydECWmt?dp$xd%@t3DTLoZq0Mg5|e?mxudDupkbSqOBY$rT%9VNNmBRy z)CP$N{OSIaxjptTMB|LQWTY`W{>g~lSSWhaC^v#C z;w<8Uum##@BXBpDLiDvmQ(xWgZuO=Qt6$S~`c55NaQTDCo9~Tz^iCAb-EAAG(jL2? z@t+p1WGDz=T**>ml8{NU|8DN3!SYJ4$bK&o!DyhWJ3cb-`I_oaSFYqD!d zBSH$b2O!}ZW4-fBAJW z&ywQdgP&iZN%KQ%8$>!7`rG2Ap;5Ou`xzKVnv<>w(w?AOzx~&X38lY{y!-M~q!0!c zpGN#wAjYKut1KtHIBYPi@a=#3fU`owwvby>oq8|L{1TFw*YM?yq|2@Fv&_OIvVj}0 zO`W7eFDl_WVXjlSoz}j}#INvN31vyZ$;-3%lrhDO9g$;%IcxFmcZ$!BkR=D#bX zn)Jj;i8~w>L5sG0z0xSN94#{HFh=uKtW-v8s8`R|K|n+#;C3`V*&H=8GGa(3Cci~m z4j1xG<|{q=GLI5nMI)EQF9GY~;Y)qD{ zw}s+XwS+vT*L%G|MLT(S)o^!obfn$j!|!hsj7jy0(~cMir+sF3rt0WqTwm~P#%Z-X z_6e6gsjX74E%Xca>pg)$R3aX$v1i!*DZDgSlYt<%F-|@p;M+$8(Wk1@4Zgn1?cok> zMQhWg7FcX^)hWP9&MgtxGZ1{R4EX2Jy#X(6UGBFZurc`yl~0a*BY)Y>k?pBLvSZZEYiEUw8M% zJX7pef7L$Z2rNlf?U|M*r4fxn1rvpC)TaXMcYnwuuSXeplejKsjWgdW5d7?vt%? zG&D4`)tHzV5U04bVP54N6=&I5;d(GbQtT#>I*V3vd3+qTrTH(pa+h-4;7mC$eh~y5({xhUP4W z%op34Dx;D6_ZZip0HY`ul# z7xtW%KYxe@V2jN%So)4) zjUvOgOxhbcV<((W#Zq&np`9Vf^hMm~xmsnG?P2)E70Hvrt!x_tq0p}@jzg!%+pI=h<`jNcVzqZexjLeIApxRuQIr% zPv0Zte8MNDi8L>K<9@+XOTcap5!)Re83%fUtb#%V5Mbisfok1@Q@QE|a}-{6@+9ae z8$@|pP+B%@|?s^j7?4B7%-Yb+Ur= zNp<3}Xyn5Po=ab!`j4q@&DHTH7cIQ7T!^4r>rGr?HBPTP`JjbkkbL)$Sgq2Cw}Ep%}N_naNBk ztXqdNysxTp_bu8r*Zpa%cuFxQkM|(rifdji<>jv!v0u&^`%9o}-mS*#+dJBVy9aHQ zss6mVuP^+cRrP{^Q85&cYNpcj%r=)d6qiX4Gz3r;+24>;`gq-TNwdM-eU*;2oGQ(V zkB|R#YTnfF>`Oq0Hal0M@At*5@1H?B5BVctDZhN_kvFE=LV(*(B@<^oQ+ec!!1e9bp5Re08vv&x4JqEH++87J7{VX?o$mt;pj z1meN^(JHB57#4+d3%oZo{jx-t(UX$NlG!O+fzh zQqW)lg(ABX$_g?-`sc@^hapRdw!qvgFLd}jsZQ1=OuBTkt-#=WOdz`C8!1buQ{_m- z&3%!KPiqh@3iVsRA+b6@q@goFB;6~$^rD2wk3PTlG3<03AK7+mkVhpj884A?aHwIU zOi&KJpv2)yNluOtr`TsorEt;vVO6>-gbr43V-u5Aa|l)YH%Y8<_j}AlF7(GxP{FX| zMU57vHLPYwoZ7d06?xKE8DA?ZD&h>^@bmHUv9VzVc-n?Md`K_cP$O3Ah&T3A=?I1Lft3?)y*ywArm7< ztl5=L6nj_T1)zsj#6PKHeLd6q>&RlMKDRtlAN$ONcBwbhbY5|agdq*qxX7MFKtKSX z7z_*y+9!(a`){*zUne-{KH(`@FjusPQ?H;&eT^u(D7k>Iw`$C=uI*Ad<#Y8RrPbaP zn=|&OgQ6*oPqpa#a4znjLCyVMI4HT14moq9EHdS27QmtrAil>C?7uKDh9T zB1bL9hnie1d(?ug;fIS(L~{FfLYz4~3QB{s1-7 zm)Jl{E`Tpe+)f>qJ7(Emv1w3t<0kma&#Sp5QE5g+xkpHlS4wG#jB60wDJtu|x6-R+eR@{)4kh0=FZg{~;E;&a81?HRv7zWiIQO+7&9wUa%u;iRTz6}W ziPF=~r^|K22{?t)q()4a3}K86Mypec)@U`evBhD=aJn@{9nwofMRx1vyl2h&)dFcV z8WCQ8^B(-(pprB4u2V+cmaJC1PfImhMfd#1(X^G-dw*Z4kaRJKBJG6Xi zU|h4*8D<{$@aIo_{qfhQRmH2_Cs*dAKf@d)?8UDnEfcW)V9`Wg_7dX}4&|9Km0ma} zvg+2H`5&%%pBoPvOkliwF@3g5`KB!6^|K$k)sAu}W|knAMJ?0b{GcOHR#xWY=a2fb zw6v5EAnGTuvl-a`%)LQD@L+?9%VE>mfp}xxD+g6=Izpk#D=q)%;APrO(`K3lx48+; zDD43{;V+R_W^doa8dJfy3@uZan~#1{Z_-!~sJ=oaZePo9s8tFSJGr(!ow*ecRm%;# z?j#`+;g5N$t>!2`CA{>KLzta_x3anjPK(`HqMYW|Lap zH0dM@??=+@!OGtLvw&5VEsh=epwv*A{uf$=9G1Bta;8P3h|x1BUo4U`m_8-inZ;$YB0}=wX_zGnr*A=eFUt;A;No#y$KC?g z?b({~mKF+T=Exp+Oq9#g`$U9L#XC)hNm}9?9=K2=$}JyA2laEjw5l`oLxce?Z#$EK zidZ=A#+!D#*KCLsax1}z=I}ak;Nz-bwaZSRa1Cnye1Qf-yW=G>@7uD_`GaWd61RD@ zG)^+y7v^(776?1H!$+$S4<(4sWoZ;COOLt49N?PFlxjR~hp?^4O-^=*6{JIdio!ti zB#DF6xuFM$x#i z`n#hLo<6tYr+=PcvWpkd_Ue#BjQX32(Pbg)<$^EIvw^!6P0zEo9<2_@)P+ti6YHj^ zU|3q%AJF>x`Y~yb6$5S2^&=^Vz9`gGr?DY{f}~azsyf8(J57&1%Hx?B7T_}Mtt`T# zmJw&j=J{m|l4t|HczYXUkw7S{ZJ0nRhwbps3Z1DC)vQU*Y_+`8k@mjx9%4u7b=;3A zPwfVLNNk%9v9um>LjCBsC|R>l?5f zk%ykr#EMMm05AIC_GMyd(|WVhF<`8-!9A14`a3PF0DYR(^vtP4W%^iv@J&WL;j1Fa zGB5q>3~0F1hdU+NcwG378VhA(V`I9)E=X`X%M@c39(N@_6#=wQ{UQIr%-#Vlo$ z90f>c$9h#b?WqA)-<9zn4$*>W=fQv9Yke1z>q%N+$rn+PxQkKfgA`b`go6{#$PhNO zftP_V9;xK1-H?TdAg!m)Te62 zDXjNhzm6zAxoP_6>tTcCdUwq&jhOF6U%KWk-iL%7Rz?gCDk`z809h>l@DS&2zC;|R z_?=xH8=2&A&u9CE@0cZiR|e}I=16fK@uy+XQM1IKqLKA{7QlAJi81GLTg1e4C>j4m zpQB>5-m9;YVal)yhk{2=(=Sf;IREBi+|2UlN`z_?!?at&+_juZl4yoQ+ozKh9=F&q zBAr>a+fp(a77mr5kZh%584*TWO3cuYRr8Ff;Zm)#b5{{|VC~BMu@aV3-yx4oEFir# zjitr#KAFQxzNbPb(!AA;pk3}$_dJw)&(|GL%Do?m%eNM~=VcbIK2_KwaIoACeT53T zo`lj4wqGl!uyHcChjZVlVNI3X~n6pWN=IM6J?$QtR_8kS3Va>3?3v2 zzER|2pS?D-;t4niPFA6BQHZjjKx}H=xzBonUuFxw%&;7_42{n-7Jv4#9*-v-C>l~k zuTgTJj7R-m`uO9!z52C3tMt5)U6S+leb1D4JG-uTg29>>ZvO-8b_nXHlkWO!plgG+ z#VKm#wvU$;1evt+356r=FJzu@?1sjdeGofV(kksnYxcBUxUE4-OiWBmOWSsl#Av^E zpy62Iia|vS`rsmBhC_(Tg@U5A*+nA+KO%Db-91+AYQ}aHB+)5b!TjUcgVed8QoXFW zFR3Z+F+bmD>u$CzcYC3uh>{_7FDM%q#8kMJ2FlQNsup{|=S@YOa}TG&M^p zOL_HCDzbEcWwtXZ5T!Mpt&eW3tbfm*)H++6V(HaMvHKI>LUQ~~CP}g2v=lK=sQ;;J z0SUT=-%0r{GyUVdGPHM1fK`8HA^(&QZ#5aaML@ENnQ+*;+mo3Yare2QEW`Ca##+u_Wrq#bNeb5nuLS|4K3pR`}c%Lg1H|lNS>d+h@~fH zB-{c@1RfRD6~L8lWbFQA?v|Y7+*Ke(sI7L~Hh)TWU#p?XbqFPN)m0dls863RLC6;PSg*2Gb3Uc2K`e=YjyByh9DK5u_EF23pg)HbI@M)?2V)Oa=`S!N;jLH{*uRrM3^Pyp2!U?&H zU!QCi6&2k#>I;~><<~3BrX8iCBIf3_*ie{auV26ZKqdy|C)A&yVaQN&#KB9!Mkg=F+#AzZvN3YwKR{{B*BrG#v4VGn;EA8P;Isgjc*m%dKn@6%}Jg@KFOL`r2-1$2`oF7Fq$(bM1DmkhtkZ&YN&aj;t+`G11Sf>}^0Q?)#;BG#W!3Lii{0DN#W9$_F^SycsCC~5!zfk!mr)JLnkLm6;5mzAENCS_Fb zEKm?YLhEt(EaGyl=F2c5ryrP%mKIiEip2W)3F!>G(ENr9#C< zu9QL(1F3=o+g|z_@?}=zfTB3z5cK=oICH$5rsPqyo1F;ksR4R2_QVmLH0b`J=x!f zM@C*@eFwG;+xOds7mow$%$vS(MI1W;1(dUs)T;#UzOS$EaX1gi(LeE|zPd6Po+FBo zQ&8x*_TDKQcaI%KH$lZ7$Q=8VrL*Zm&|@rQWFEJ@>m4M%+hci8?#WY*dI65n-Q^Zj zGU@3eI%f!%!xHouJB`9;<0P?AoK;+e0I;9F*^*`^W;Ui*&2|QK_%C71$l1*gX;8zQI34Q!KQe&~%Uvw4+x~-Xb?sJb}2MRF%BtDN~UM(FrLT0N}ukumS-hE^}6E6iOJ0;Ad5-cx{i9vM}mdvhNi6v3YGB z9Y(UntOcVMLV)v9#b7D3r@xU?<(iO0uQg7t%_v12G)JGCN za-Y~=RiNbrecSvJor0H1`yh>l%-o*gjL^~1NngW8kD|kl{^N!nh75|YB?HyHkNP+j z`H$B6qPLee3=CW$CeuJu?T8>*$HA%_QT)-E$)eszEwlJc&uaDChoQ{EP?3DQ`Ff}I z{>N6_F?k>*cUsT{jZO5xCj5Q1%huFM=3C$EV3UFPAT8}aixsEj08Oy;UB|ak! z-kA~=_3u)-Nfhc$%%%eg7frfhC)tnzitD|i0X$mbqF<=N=SCxOPmE@Li+5t8zKbsw zfaS=P6q}R57}U$jV4)CY;&)wZ0>Ad;!%R{W4f&>t4TmdtHg!bd?$>QDMuLf+)V?H6 zb+ur{ve0&r+n{mEc=h?e0;8nko#5topjo)>OsTR~S5?_e6gFkzL8%4#!>5qp;^PND ztpM5F+A7*>tgk;0#(GjpcuO?$$#~B#W5z0IO102g%=!s&adBmhz~1x&H38`}_M~8<-yATk|Lh0IFOyD<+`xn(|Ls zvH^)VaQXQ({2?kpA!}cLY2fX;n{}JlEwhZ>A=9eRO$nu~-iQ&Cq!W5KoBlQf4B{lk zuNOa#?W*o5H&B6*=%lgF8#bTo^I}ukWUd1;L29arfNye1haJL%ysq2kW4Vb^EU=k! zwlQByWkgULy^G8SKI4%ATx>g8OtlAf#018gFBSMle<)=jU{Pm&`*uh>S1DBkgHcsg z_41o+mQsc|7{7fnNyG?!^9T0UTVgZ}L73GS0NDIYx4QH3gz>d#X@A&a0Ll@nb|JLDa9#P* z)Ra0O)sslspT&137=;_P>he`n(fyh=@?By1^e7V_{o8l>lqFTI9j6*qxM=>JWgJqv zWsxA%ECC-Jv{bMcW8u==4YiYYeTZgO6Igj(Nm)Pl7g;aQal#Pe%qxb!?;q6n;*=P& zbghu##bJPRWL^NuG21ZolOYg1C^*#4u^=Rh<22IylmxSE^e{*`(UxCB8PU=%IU8%}(6-dli0zISm6L=cqL<0m&lm~T;e)!jA z@6~c79QZ!MV(iAV%ou-6XUf0T^TT|eCE~Y2{;kD5Vty;(V9^X_GOC-wSM>T-{!lY9 zMWW;^`n12l1twA?n0-rUV4TMfMDZ~Fe6Dpy3U7VkKvVhD2>2uF75`ivhcw((ea-sT zwCC=oT-Cz2^ccrSTnlo0ad8oBTei(IWB&bfUFk$%i(VlQUe12QQ;|q2NeKqY) zrjRREYzI^25knJ{_K^p*d%IO39=-_U`}LOJ#5d@!{9c#ZY{0DZZFS!X zlb3xGPR~)vD&{Jyao%d`d8#=Mq#7l)h-x?B_^zR_YMqs?G<+Nu=f55lb-2FG=0OK4 zahXLAhlV@+vY9?|%I&Dm&^Z8r4Y#{64(PE7Jo_a~_TtAk!GTobZBAvWcc(Qb=b^>J zLP1*b8fKlZ)(kBBK36bP(h0CIcmm|}L*ePB@a5yw3?aXu@La9D%f$B@S!&+{uG7aH z_#FvKjI`3p*x9i|ntFQZ`T6-fBGcavk(!M@NIfjG4Eu;gHeV%CFIKLz>9z^~s8OxH z2QqiJ3+ElF;LgPsk|b@({mYEzsWhMe^(Kfxv05p;6H!5P`s?Nxi-Nk_K>3%px-~Wv zr2(-OxgyITTI>o$M=Y{=DvXa)`A~ntlOBy$n19?_VM*V%T!Dd*275|ma zq~q$PH#@qecohX#hBxK&TpCyu;=peS{LEbG+5>v#M;ojr)vkM8FCknnyT1Ibr6H^& zPBav2oL4_>4*YB!miXU+af8$@n`lpK)<~O2W-1_k8a9FgR7U~T{T&Jcj&NX^U;PrB zqLR@n(pezz-iFR9)^+2wd#6$+dy4z+Y^4DqCW-=;iwJCmg{$lC1}|L+PJqg#p^MW%J05k-k{DWvR$4~@|sFu`txtkc$ltVH9KazB)NWX8EB(2 zBV|B+@>~rBw?qRy0E8j&=1$MH(rNx}Os`cZMJgKp?NH-Dg7>wtTe8zL=@*$$fAQ0P zJ2#2t?tMsdER_3x^B4y?~?6om-uKQ?W;g9?-YopvYJS@5m{YkJGVL zAJoZ$496O#wx6eb8yo=JRxzq?>3pWwnXW`S65ia8OpG5VYx`jH+_%zg92D}x58STg z1F09!G;cGs$8Bl9WIPaP+vyuz)L3t>)^Kv~h?K@p?R)1^Pe9F*AIi6d#3~o(o6X*> zAagRP1@r`QH+Fa7t5?b5Sg;iK0@)(68%*=rpyuVrASc6YY$iP&FvSsCIdaic6d-C^ zx}E4M$qkzV!;M!a*rL4i@lJJ@kIb4ixmn4+n&z2(g)QkNS}x*}o%`0=T52ter6x{t ztq`<&k;-Sa_kb%Gun6(LhcR%m4ned*#4c=Yg|e1Hb`PQ~A$_DIIJQvdbT$>bcOcNw zs-KszPKW-{oeN+Z)h?g#48QEhz3NoS$Q0=;Za~W(%vLRvsZd9ShdhW@L+BN7pXjv1 z@rpOOJ-!3eMC%leo17|ulW?xG#Qu`rL+c{|GG4~XXcy5vzk)^G_#U2g5YNA|LdH~o ze3%M%9katM+)eg)cXF?V;qq2A10!0Ru%@xIXjwDX`9h`O+XLE^Hw(zLW}u`aHd4M1 zknz3t)>Qo$?Hz){KzGmL6D*VFf}rewN|Har$%@+x^^I9*{2dF5I!Lua<8(59ez4U4 zLx2O9_JabK{;j}Ov;U#MHP<4p37*?F+rj~eGAcv^Qs_WV&wXEjcKWAdr}7GqT0ock zl6A~^9ZlZ1!c*IGX`^HO3MK2YoOvL8@}24cv5V=KzFz`9ga$qfM^iWTxYY4_ zDZ+H;-SRp30s6F{Pe)t3iCuV!2=JJ?uU~TZC{M|Z%bzG`KLG5y@fxPmlK%#9_Qk%#s24cp>lIu+5! zcdk$zP<|8|d1HE&v)O*SJ4PyJ8-k6SKVA}?lthEw1R2*1+|Um#`pFZ!g4ubGvlw z8F!^uw)#r~s2Pb;NfFdm?;@+S&nDe*b!Rt+oZ5#1%y+>3W{~~DA>z^z(vv}Q@=+d+ zs9tUI)oSb0dzM>fS=QF0#!3$-Fe|q;CcRCa0-rABulCjFSH0vZ zz4M>lPphWjgLoE6_<{1O0r|bu7Am5bfiXwzdU^0YHJ(8om8=uv`YXzlhuIhTB`t&2 zD%>LfbNjp5fZpC-Ffuvi$%Hq$t;BlbNGKf?B+_v||3ZaJ4h*C{S&1g~&pP+g&hfUb z*o-t$xJn$~3+X;r*b~tzP>})jj!)WBbkx?9#5v4x9xulAW_oK(_iBPus1+S{7t31! z1>sQ-T1=6i@Z{zIoBe&!aR3KH)HynBzlUe=3ev`xQe4L20ZA+y!5hCu!JqM3S{-dymXq{iM_i9&W zUa~z0{vN)CXd%$mV^Qz@wzwt_`u(@h_UJFAeXrLOB*3Qt{mvC}M*ZN;JtxTonn)-e z7>>dJ4T=BrJH;1yk!mY;V;hW~K0>vB*$z&Oc%}V*pT?Wk_sRrFNJuX0HffL0_9OV` z9$5IlsC*n!l4hH(O~zav^P$DYW??AeFBSW2!Ww{BeD_Ugl-`x#(RHMX|51v&WDr2 z?PUb7WwpI~byGi)p@g3UvU1xWouzLRy0X|yYfG24`o`l^qz_6SaB5yY z*g4>OS2M%5IuUGI%D#madg}fgp9&6(zlDmU-iJtPEq-O_Cp@1cilRBvm*Y|J65iaebOvL zf2l1wDFF?5n<}_R*$9`2WV{?A>?gd~5pt*~0}{0j-XEV#^rIYEgM- z*%4((&_6-Ye>dvL|7zKy`z~Uqofl0JiI9(|99#mzfSx)K~rLP7x7}dQybj}-f z06fO>O78nr+c({|ym(#>es+eSf$T=;)&K{d?EBrMfR zL1?hQXi=qkB7e{neiz@eUP0JE3KOEju{EKi$N^u^vSr^M%Qy7q%DcwyFiwX@v|5lj zWzuJk<@nW66s74;9v8{M!is9U^_uckM?ON>>Dv1)95!AS9E z*$iV>DQjrhHEX4|Rm!#govs3*i1NK24$5;SkM6fWZ&sKuDwwgUZ*acV_DJ4m?DTw4 z>2{Jv2<=W7U+rX=NT=3!EJEL5W>9K4$o%opbXFw(zgJGc!>M#}))>YU21@5CZn-@% zV2o|f=3Ck{B*~JA9%y(C1fyu$}gr7P2XEcxF z#-;GbW96ta^|!oyon16nBHCE&Moe~}FaP&x^-j^}9^2n)PGI zqFey8!)6!!bx+G$g1y3H6&tI3Q%&Ax+_{~mmK4)b62uM1 zj!w0rOj9oO&(^Y&9k%E3@}+%wBRBW{To2O@3-y}2_8cRjnAE2J$eT2fW%n|dtvmTE z0oB+PFK#9*{+9%|WJwF3yB3U;lT^urXITVoFKH${e66bp(Sl^z!F(4WV-;{_?Q9bz#{VU8V2erI(&-zw_VZ{rz#v7EID%{MWT%3l{#m8jq zHcpSGe1Qe@7zKh%j;Pi`Vbribf^rrO$3aAuDH=P3f$C2^`bE7=zr{DKS@?hv&TiFN z;O4tomTnCM!}JA(enx$4Y2ONbr#3$x2S7zF;Ux ze1<_lKt~ryh>0?0{%~KQHFvwAp4U{XbyTUsJ(+gnKpO{(YNZgkx35s1rKBJ151RZe zV-C@icQ#ta8dpdj@3H?V-(X#*#fB{oGFC@XDMQmE34K{GOiQ(J`74KoTA!snPaWl%u0dYTQAw!epS!`bd2F*yME^H#AZ7$X?&WX`@P1? zbx_TE@-1UYBIlYTcuh=A0|Nt#$H~O-jyh6y!=WDirId%~^avqP9Cp@h&_hb5mmu`M zjtT%9jeYs|o7Y&ob&s3E>*7<<8{)~tr5YpE`iWfUF)}j6RdzT|nEBt-&ECAc=* z#}5}6a#odS$8+Xi;P;SLhhI7h1E$!>vP^S;Q@Slm;WUM{wYo6cLh#V-QA zK8w)t%iTs{g=g(cV1NED9d8R9pbXP)p-S*iflC@}qD_t~g8@Zp_13f@YoOf?HIM!O z!P(ZG7ni`EdXKYXE`S;BgC8sJ!3dSGb!%4|aHHMF_+J@yQp4m&_2)iiS-?-_{_`8d zw1HB)^!09#Dsb|06ZxQuzS`)1e(9S2D)`?m%lm`>!Ls~a7f$`Z!)XlF|EP4U9;hCd z14p)Do3zs!Q^sgEp5m8dLa7eZ@!=%BA}UxJG_))fVVf1uWF_&pSl?u?z9{$tx(*W+pFbn)9{#B;TP3S{ z|7x{2;gh$~AB*xEIa7t6s*6cnks|Y)8?O`qocF!S6y#D4T=L9P>9zB98X{nY`)J*u z%}OIO)#|+h0J;bM4K(Br_YS)$uWH%E{8`U-O5K&eZOS#Lt7UG!E$Rfz#)hjErY+QS zkA@^X2#`KKm^j)-MzH>Nar{OVz>%1=A^=<*$vnWtq2}}uxaF6u`aib3u2pE=3SXOR zp=wP^J%7-yiW7gS(^yhuiHM|$bX6-o7^4*2&0Su1yri1jLvef@mU3X`tF&Q1Z-`%G zxD>eAv@vA@>B@&7o~l6qvOKpw#azSH-bK6qH4!4PEW5dEHSbg&>sFsM3X`}Mbd8v1 zZ3^aj{9s$LuMl{QlYxwp!BrH}jG}i>_b8Pufj?0`P&@-F)knY|pNv9hsvIp`3L2cM z_cQL;mV8$jl*0SRwqZN6>wI2Sxq_V8_9~;D4#W`$DV@2?o4Vv~z6Zr^mpAgG2g;|7 zBeE7jf^+M(i!Di5k(MQGxbKq}wG~oCI?_+wkW;uA1Ycc$L ze$4V_nt`6iOlLI$QB+!_rz?=`tQAY4aQ10e3pa~@2rd)jC@>NoUhw>JXKb(ByEAsy zw}3whsL!4%y&Hh7atRY7l}pA-l>IcWHq>$jes|zqexvR4`L3(Pb!RJ) z;Q2a}-fIpXA$I9?x%>Q*Wq-l)g6jP8G95YZ4Xd-?`8&#BSnmf<@0(nD;9qV|jh)o{ z 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 VnV}YGd0cE36P6VEETH-7{{sj_a3uf$ literal 0 HcmV?d00001 diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java index 2a5bc5a226..63d7589076 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsDialog.java @@ -58,6 +58,7 @@ import ghidra.program.model.util.CodeUnitInsertionException; import ghidra.program.util.ProgramLocation; import ghidra.util.Msg; import ghidra.util.Swing; +import ghidra.util.charset.CharsetInfoManager; import ghidra.util.exception.CancelledException; import ghidra.util.layout.PairLayout; import ghidra.util.table.GhidraTable; @@ -69,10 +70,10 @@ public class EncodedStringsDialog extends DialogComponentProvider { private static final Map CHARSET_TO_DT_MAP = Map.ofEntries( // charsets not in this map will use StringDataType and will // set the charset setting at the memory location of the string to be created - Map.entry(CharsetInfo.USASCII, StringDataType.dataType), - Map.entry(CharsetInfo.UTF8, StringUTF8DataType.dataType), - Map.entry(CharsetInfo.UTF16, UnicodeDataType.dataType), - Map.entry(CharsetInfo.UTF32, Unicode32DataType.dataType)); + Map.entry(CharsetInfoManager.USASCII, StringDataType.dataType), + Map.entry(CharsetInfoManager.UTF8, StringUTF8DataType.dataType), + Map.entry(CharsetInfoManager.UTF16, UnicodeDataType.dataType), + Map.entry(CharsetInfoManager.UTF32, Unicode32DataType.dataType)); private static final String BUTTON_FONT_ID = "font.plugin.strings.buttons"; @@ -443,7 +444,7 @@ public class EncodedStringsDialog extends DialogComponentProvider { private void buildCharsetPickerComponents() { charsetComboBox = new GhidraComboBox<>(); charsetComboBox.getAccessibleContext().setAccessibleName("Charset Checkboxes"); - for (String charsetName : CharsetInfo.getInstance().getCharsetNames()) { + for (String charsetName : CharsetInfoManager.getInstance().getCharsetNames()) { charsetComboBox.addToModel(charsetName); } charsetComboBox.setSelectedItem(getDefault(EncodedStringsPlugin.CHARSET_OPTIONNAME, @@ -908,7 +909,7 @@ public class EncodedStringsDialog extends DialogComponentProvider { private void updateOptions() { String charsetName = charsetComboBox.getSelectedItem().toString(); if (!charsetExists(charsetName)) { - charsetName = CharsetInfo.USASCII; + charsetName = CharsetInfoManager.USASCII; } boolean scriptOptions = showScriptOptionsButton.isSelected(); @@ -930,7 +931,7 @@ public class EncodedStringsDialog extends DialogComponentProvider { settings = new SettingsImpl(); CharsetSettingsDefinition.CHARSET.setCharset(settings, charsetName); } - int charSize = CharsetInfo.getInstance().getCharsetCharSize(charsetName); + int charSize = CharsetInfoManager.getInstance().getCharsetCharSize(charsetName); updateTrigramStringValidator(stringModelFilenameComboBox.getText()); boolean requireValidStrings = requireValidStringCB.isSelected(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java index 9e60ce54f4..9fcb88bcf7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/strings/EncodedStringsPlugin.java @@ -30,9 +30,9 @@ import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.data.CharsetInfo; import ghidra.program.model.listing.Program; import ghidra.util.HelpLocation; +import ghidra.util.charset.CharsetInfoManager; import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakSet; @@ -52,7 +52,7 @@ public class EncodedStringsPlugin extends ProgramPlugin { private static final String ACTIONNAME = "Search For Encoded Strings"; static final String STRINGS_OPTION_NAME = "Strings"; static final String CHARSET_OPTIONNAME = "Default Charset"; - static final String CHARSET_DEFAULT_VALUE = CharsetInfo.USASCII; + static final String CHARSET_DEFAULT_VALUE = CharsetInfoManager.USASCII; static final String TRANSLATE_SERVICE_OPTIONNAME = "Default Translation Service Name"; static final String STRINGMODEL_FILENAME_OPTIONNAME = "Default String Model Filename"; static final String STRINGMODEL_FILENAME_DEFAULT = "stringngrams/StringModel.sng"; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetInfoPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetInfoPanel.java new file mode 100644 index 0000000000..b9c4553649 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetInfoPanel.java @@ -0,0 +1,161 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset.picker; + +import java.awt.Font; +import java.lang.Character.UnicodeScript; +import java.util.*; + +import javax.swing.*; + +import docking.widgets.checkbox.GCheckBox; +import docking.widgets.label.GLabel; +import docking.widgets.list.GListCellRenderer; +import generic.theme.Gui; +import ghidra.app.plugin.core.strings.CharacterScriptUtils; +import ghidra.util.charset.CharsetInfo; +import ghidra.util.layout.VariableHeightPairLayout; + +/** + * A JPanel that displays the details about a {@link CharsetInfo} object. + */ +public class CharsetInfoPanel extends JPanel { + private Map scriptExampleStrings = new HashMap<>(); + private JTextField nameTF; + private JTextArea commentTA; + private GCheckBox fixedCB; + private JTextField minmaxTF; + private JTextField alignTF; + private JList scriptsList; + + public CharsetInfoPanel() { + super(new VariableHeightPairLayout()); + + build(); + } + + private void build() { + + nameTF = new JTextField(); + nameTF.setEditable(false); + nameTF.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + add(newLabel("Name:", "Charset name", nameTF, false)); + add(nameTF); + + commentTA = new JTextArea(2, 100); + commentTA.setEditable(false); + commentTA.setLineWrap(true); + commentTA.setWrapStyleWord(true); + Gui.registerFont(commentTA, "font.textarea.astextfield"); + + add(newLabel("Description:", "Charset description", commentTA, true)); + add(commentTA); + + fixedCB = new GCheckBox(); + fixedCB.setEnabled(false); + + add(newLabel("Fixed Length:", "Charset uses a fixed number of bytes to produce a character", + fixedCB, false)); + add(fixedCB); + + minmaxTF = new JTextField(); + minmaxTF.setEditable(false); + minmaxTF.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + add(newLabel("Min/Max Bytes Per Char:", + "Number of bytes a charset needs to produce a character", minmaxTF, false)); + add(minmaxTF); + + alignTF = new JTextField(); + alignTF.setEditable(false); + alignTF.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + add(newLabel("Aligned Size:", "Byte offset that is valid to start a character", alignTF, + false)); + add(alignTF); + + scriptsList = new JList<>(List.of().toArray(UnicodeScript[]::new)); + scriptsList.setCellRenderer( + GListCellRenderer.createDefaultTextRenderer(this::getScriptCellRendererText)); + scriptsList.setVisibleRowCount(5); + JScrollPane scriptsSP = new JScrollPane(); + scriptsSP.setFocusable(false); + scriptsSP.getVerticalScrollBar().setFocusable(false); + scriptsSP.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scriptsSP.getViewport().add(scriptsList); + + add(newLabel("Scripts:", "The scripts that this charset produce", scriptsSP, true)); + add(scriptsSP); + } + + private GLabel newLabel(String text, String tooltip, JComponent comp, boolean top) { + GLabel label = new GLabel(text); + if (top) { + label.setVerticalAlignment(SwingConstants.TOP); + } + label.setToolTipText(tooltip); + label.setLabelFor(comp); + return label; + } + + @Override + public void setFont(Font font) { + super.setFont(font); + if (scriptExampleStrings != null) { + scriptExampleStrings.clear(); + } + } + + public void setCharset(CharsetInfo csi) { + nameTF.setText(csi.getName()); + nameTF.setCaretPosition(0); + commentTA.setText(csi.getComment()); + commentTA.setCaretPosition(0); + fixedCB.setSelected(csi.hasFixedLengthChars()); + + String min = + csi.getMinBytesPerChar() > 0 ? Integer.toString(csi.getMinBytesPerChar()) : "unknown"; + String max = + csi.getMaxBytesPerChar() > 0 ? Integer.toString(csi.getMaxBytesPerChar()) : "unknown"; + minmaxTF.setText("%s / %s".formatted(min, max)); + minmaxTF.setCaretPosition(0); + + alignTF.setText("%d".formatted(csi.getAlignment())); + alignTF.setCaretPosition(0); + + scriptsList.setListData(List.copyOf(csi.getScripts()).toArray(UnicodeScript[]::new)); + } + + private String getScriptCellRendererText(UnicodeScript script) { + buildScriptExamplesMap(getFont()); + if (script == null) { + return ""; + } + String name = script.name(); + String example = scriptExampleStrings.getOrDefault(script, ""); + if (!example.isEmpty()) { + example = " \u2014 " + example; + } + return name + example; + } + + private void buildScriptExamplesMap(Font f) { + if (scriptExampleStrings.isEmpty()) { + scriptExampleStrings.putAll(CharacterScriptUtils.getDisplayableScriptExamples(f, 7)); + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerDialog.java new file mode 100644 index 0000000000..24d6a139c9 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerDialog.java @@ -0,0 +1,78 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset.picker; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import ghidra.util.HelpLocation; +import ghidra.util.charset.CharsetInfo; + +/** + * Dialog that displays a charset picker table and lets the user press ok or cancel. + *

+ * Call {@link #getSelectedCharset()} after the dialog closes to get the selected value. + */ +public class CharsetPickerDialog extends DialogComponentProvider { + + /** + * Allows user to pick a charset from a table in a modal dialog. + * + * @param defaultCSI default charset to initially select in the table + * @return selected charset, or null if canceled + */ + public static CharsetInfo pickCharset(CharsetInfo defaultCSI) { + CharsetPickerDialog dlg = new CharsetPickerDialog(); + dlg.setSelectedCharset(defaultCSI); + DockingWindowManager.showDialog(dlg); + return dlg.getSelectedCharset(); + } + + private CharsetPickerPanel panel; + private CharsetInfo csi; + + public CharsetPickerDialog() { + super("Pick Charset", true, false, true, false); + + panel = new CharsetPickerPanel(null); + addWorkPanel(panel); + addOKButton(); + addCancelButton(); + setDefaultSize(800, 800); + setRememberLocation(false); + setHelpLocation(new HelpLocation("Charsets", "CharsetPicker")); + } + + @Override + protected void okCallback() { + this.csi = panel.getSelectedCharset(); + close(); + } + + @Override + protected void cancelCallback() { + this.csi = null; + super.cancelCallback(); + } + + public void setSelectedCharset(CharsetInfo csi) { + panel.setSelectedCharset(csi); + } + + public CharsetInfo getSelectedCharset() { + return csi; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerPanel.java new file mode 100644 index 0000000000..5626cb3836 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetPickerPanel.java @@ -0,0 +1,103 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset.picker; + +import java.awt.BorderLayout; +import java.nio.charset.Charset; +import java.util.function.Consumer; + +import javax.swing.*; +import javax.swing.table.TableColumn; + +import ghidra.util.charset.CharsetInfo; +import ghidra.util.table.GhidraTable; +import ghidra.util.table.GhidraTableFilterPanel; + +/** + * JPanel that displays a table of all charsets on top and a detail panel on bottom. + */ +public class CharsetPickerPanel extends JPanel { + + private GhidraTable table; + private CharsetTableModel tableModel = new CharsetTableModel(); + private GhidraTableFilterPanel tableFilterPanel; + private Consumer charsetListener; + private CharsetInfo selectedCSI; + + public CharsetPickerPanel(Consumer charsetListener) { + super(new BorderLayout()); + build(); + this.charsetListener = charsetListener; + } + + private void build() { + + table = new GhidraTable(tableModel); + table.setVisibleRowCount(10); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + tableFilterPanel = new GhidraTableFilterPanel<>(table, tableModel); + + JScrollPane scrollPane = new JScrollPane(table); + + CharsetInfoPanel detailsPanel = new CharsetInfoPanel(); + detailsPanel.getAccessibleContext().setAccessibleName("Details"); + detailsPanel.setBorder(BorderFactory.createTitledBorder("Details")); + + JPanel innerPanel = new JPanel(new BorderLayout()); + innerPanel.add(scrollPane, BorderLayout.CENTER); + innerPanel.add(tableFilterPanel, BorderLayout.SOUTH); + innerPanel.getAccessibleContext().setAccessibleName("Table Filter"); + + add(innerPanel, BorderLayout.CENTER); + add(detailsPanel, BorderLayout.SOUTH); + + table.getSelectionModel().addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + CharsetTableRow row = tableFilterPanel.getSelectedItem(); + if (row != null) { + selectedCSI = row.csi(); + detailsPanel.setCharset(row.csi()); + if (charsetListener != null) { + charsetListener.accept(row.csi().getCharset()); + } + } + } + }); + TableColumn col = table.getColumnModel().getColumn(CharsetTableModel.MINLEN_COL); + col.setMaxWidth(100); + col = table.getColumnModel().getColumn(CharsetTableModel.MAXLEN_COL); + col.setMaxWidth(100); + col = table.getColumnModel().getColumn(CharsetTableModel.FIXEDLEN_COL); + col.setMaxWidth(100); + } + + public void setSelectedCharset(CharsetInfo csi) { + int rowNum = tableModel.findCharset(csi); + if (rowNum >= 0) { + table.getSelectionManager().setSelectionInterval(rowNum, rowNum); + table.scrollToSelectedRow(); + } + } + + public void setCharsetListener(Consumer charsetListener) { + this.charsetListener = charsetListener; + } + + public CharsetInfo getSelectedCharset() { + return selectedCSI; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableModel.java new file mode 100644 index 0000000000..236e70a116 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableModel.java @@ -0,0 +1,125 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset.picker; + +import java.lang.Character.UnicodeScript; +import java.util.*; + +import docking.widgets.table.AbstractSortedTableModel; +import ghidra.util.charset.CharsetInfo; +import ghidra.util.charset.CharsetInfoManager; + +class CharsetTableModel extends AbstractSortedTableModel { + + final static int NAME_COL = 0; + final static int COMMENT_COL = 1; + final static int FIXEDLEN_COL = 2; + final static int MINLEN_COL = 3; + final static int MAXLEN_COL = 4; + final static int SCRIPTS_COL = 5; + + final static String[] COL_NAMES = + new String[] { "Name", "Description", "Fixed Length", "Min BPC", "Max BPC", "Scripts" }; + + private List charsets = new ArrayList<>(); + + public CharsetTableModel() { + CharsetInfoManager.getInstance() + .getCharsets() + .stream() + .map(csi -> new CharsetTableRow(csi, getScriptsString(csi.getScripts()))) + .forEach(charsets::add); + } + + private static String getScriptsString(Set scripts) { + StringBuilder sb = new StringBuilder(); + for (UnicodeScript script : scripts) { + if (!sb.isEmpty()) { + sb.append(", "); + } + sb.append(script.name()); + } + return sb.toString(); + } + + public int findCharset(CharsetInfo csi) { + for (int i = 0; i < charsets.size(); i++) { + CharsetTableRow row = charsets.get(i); + if (row.csi().getName().equals(csi.getName())) { + return i; + } + } + return -1; + } + + @Override + public String getName() { + return "Charsets"; + } + + @Override + public int getColumnCount() { + return COL_NAMES.length; + } + + @Override + public String getColumnName(int column) { + return 0 <= column && column < COL_NAMES.length ? COL_NAMES[column] : "<>"; + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case NAME_COL: + return String.class; + case COMMENT_COL: + return String.class; + case FIXEDLEN_COL: + return Boolean.class; + case MINLEN_COL: + return Integer.class; + case MAXLEN_COL: + return Integer.class; + case SCRIPTS_COL: + return String.class; + } + return Object.class; + } + + @Override + public boolean isSortable(int columnIndex) { + return true; + } + + @Override + public List getModelData() { + return charsets; + } + + @Override + public Object getColumnValueForRow(CharsetTableRow row, int column) { + return switch (column) { + case NAME_COL -> row.csi().getName(); + case COMMENT_COL -> row.csi().getComment(); + case FIXEDLEN_COL -> row.csi().hasFixedLengthChars(); + case MINLEN_COL -> row.csi().getMinBytesPerChar(); + case MAXLEN_COL -> row.csi().getMaxBytesPerChar(); + case SCRIPTS_COL -> row.scripts(); + default -> "???"; + }; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableRow.java b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableRow.java new file mode 100644 index 0000000000..37e2b78ad5 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/charset/picker/CharsetTableRow.java @@ -0,0 +1,20 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset.picker; + +import ghidra.util.charset.CharsetInfo; + +record CharsetTableRow(CharsetInfo csi, String scripts) {} diff --git a/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm b/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm index be229f4040..d71cc211cd 100644 --- a/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm +++ b/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm @@ -7,213 +7,214 @@ -

The Byte Viewer

+

The Byte Viewer

The Byte Viewer displays bytes in memory in various formats, e.g., - Hex, Ascii, Octal, etc. The figure below shows the Byte Viewer + Hex, Characters, Octal, etc. The figure below shows the Byte Viewer plugin in a separate window from the default - tool, the Code Browser. 

+ tool, the Code Browser.

-


 

+


-

To show the Byte Viewer, select the icon, , - on the Code Browser toolbar, OR, choose the WindowBytes: ... menu.

-

The following paragraphs describe the Byte Viewer.

+

To show the Byte Viewer, select the icon, , + on the Code Browser toolbar, OR, choose the Window + Bytes: ... menu.

+ +

The following paragraphs describe the Byte Viewer.

Data Formats

This section describes the formats that Ghidra provides by - default.  Each format is an instance of a DataFormatModel interface, - so any - new formats that you provide will automatically show up in the Byte Viewer Options dialog that - lists the data formats that - may be added to your view. To add or remove a data format view - from the tool, press the  - icon to bring up the  Byte Viewer Options dialog.  - Select the formats that you want and press the OK button.
-

+ default. Each format is an instance of a DataFormatModel interface, + so any new formats that you provide + will automatically show up in the Byte Viewer Options dialog that + lists the data formats that may be added to your view.

+ +

To add or remove a data format view from the tool, press the + icon to bring up the + Byte Viewer Options dialog. Select the formats that you want and press the + OK button.

+
-

Hex

-
-

The Hex view shows each byte as a two character hex value. Hex +

+

The Hex view shows each byte as a two character hex value. Change the group size for the Hex format to show - the bytes grouped in that size. When you add the Byte Viewer + the bytes grouped in that size. When you add the Byte Viewer plugin to a tool and then open a program, the Hex view is automatically - displayed by default. 

+ displayed by default.

-

This view supports byte editing. 

-
- -

Ascii

-
-

The Ascii view shows each byte as its equivalent Ascii character. - For those bytes that do not represent an Ascii character, the format shows +

This view supports byte editing.

+
+ +

Chars

+
+

The Chars view shows each byte (or group of bytes) as its equivalent + character, using a JVM installed charset. Typical examples will be US-ASCII + (1 byte per character), UTF-8 (between 1 and 3 bytes per character), UTF-16 + (typically 2 bytes per character, but can also require 4 bytes for some).

+ +

For those bytes that do not encode a valid Unicode character, the view shows it as a tic (".").

-

This view supports byte editing. 

-
- -

Address

-
-

The Address view displays  a tic (".") for all bytes whose + +

Some characters, though valid, will not be able to be displayed if the active + font does not support that character. Typically these characters are rendered + as a blank square or some other distinctive shape to indicate the issue.

+ +

This view supports byte editing.

+
+ +

Address

+
+

The Address view displays a tic (".") for all bytes whose formed address does not fall within the range of memory for the program. For those addresses that can be formed and are in memory, the view shows the symbol, -   So if you go to that address in the Code Browser, and + So if you go to that address in the Code Browser, and make a Pointer data type, the address pointed to is in memory. Conversely, if you go to a "tic" address in the Code Browser and make a pointer, the - address pointed to is not in memory (the operand is - rendered in red).

- -

This view does not support editing.

-
- -

Disassembled

-
-

The Disassemble view shows a "box" ( ) symbol for each address that has - undefined bytes. For those addresses that are instructions - or defined data, the - view - shows a tic ("."). With this view, you can easily see what areas of the + address pointed to is not in memory (the operand is rendered in red).

+ +

This view does not support editing.

+
+ +

Disassembled

+
+

The Disassemble view shows a "box" + () symbol + for each address that has undefined bytes. For those addresses that are + instructions or + defined data, the + view shows a tic ("."). With this view, you can easily see what areas of the program have been disassembled.

-

This view does not support editing.

-
- -

Hex Short

-
-

This format shows two-byte numbers represented as an four-digit hex number. 

-

This view supports editing. When a byte - is changed, both bytes associated with this address are rendered in - red to denote the change.

-
+ +

This view does not support editing.

+
-

Hex Integer

-
-

This format shows four-byte numbers represented as an eight-digit hex number. 

-

This view supports editing. When a byte +

Hex Short

+
+

This format shows two-byte numbers represented as an four-digit hex number.

+ +

This view supports editing. When a byte is changed, + both bytes associated with this address are rendered in + red to denote the change.

+
+ +

Hex Integer

+
+

This format shows four-byte numbers represented as an eight-digit hex number.

+ +

This view supports editing. When a byte is changed, all four bytes associated with this address are rendered in - red to denote the change.

-
+ red to denote the change.

+
-

Hex Long

-
-

This format shows eight-byte numbers represented as an 16-digit hex number. 

-

This view supports editing. When a byte +

Hex Long

+
+

This format shows eight-byte numbers represented as an 16-digit hex number.

+

This view supports editing. When a byte is changed, all eight bytes associated with this address are rendered in - red to denote the change.

-
+ red to denote the change.

+
-

Hex Long Long

-
-

This format shows 16-byte numbers represented as an 32-digit hex number. 

-

This view supports editing. When a byte +

Hex Long Long

+
+

This format shows 16-byte numbers represented as an 32-digit hex number.

+ +

This view supports editing. When a byte is changed, all 16 bytes associated with this address are rendered in - red to denote the change.

-
+ red to denote the change.

+
-

Integer 

-
-

This view shows four-byte numbers represented in decimal format. 

-

This view does not support editing.

-
- -

Octal

-
-

The octal view shows each byte as a three character octal value. 

-

This view supports editing.

-
- -

Binary

-
-

The binary view shows each byte as an eight character binary value. 

-

This view supports editing.

-
+

Integer

+
+

This view shows four-byte numbers represented in decimal format.

+ +

This view does not support editing.

+
+ +

Octal

+
+

The octal view shows each byte as a three character octal value.

+ +

This view supports editing.

+
+ +

Binary

+
+

The binary view shows each byte as an eight character binary value.

+ +

This view supports editing.

+

Status Fields

-

The  labels below the scroll pane that contains the views shows the following information:

-

 

-
- - - - - - - - - - - - - - + + + + + + +
StartThe minimum address of Memory
EndThe maximum address of Memory
OffsetDisplayed in decimal, the +

The labels below the scroll pane that contains the views shows the following + information:

+
+ + + + + + + + + + + + + - - - - - - -
StartThe minimum address of Memory
EndThe maximum address of Memory
OffsetDisplayed in decimal, the number of bytes added to each block of memory that is being displayed. - This number is calculated when you set the alignment - address or the number of bytes per line. 
InsertionThe address of your current cursor location
-
+ This number is calculated when you set the alignment + offset or the number of bytes per line.
InsertionThe address of your current cursor location
+
-

 

- -

Editing Memory

+

Editing Memory

-

To enable byte editing, 

-
    -
  1. Toggle the Enable/Disable Edit toolbar button -  so that it appears pushed-in. 
  2. -
  3. Click in a view that supports editing, e.g., Hex or Ascii 
  4. -
  5. The cursor changes to red to indicate that this view can be edited. 
  6. -
-

Changing bytes is allowed only if your cursor is at an address - that does not contain an instruction. If you attempt to change a byte - of an instruction, an "editing not allowed" message is displayed in the - status area of the - tool. 

+

To enable byte editing,

+
    +
  1. Toggle the Enable/Disable Edit toolbar button + so that it appears pushed-in.
  2. +
  3. Click in a view that supports editing, e.g., Hex or Chars
  4. +
  5. The cursor changes to red to indicate that this view can be edited.
  6. +
+

Changing bytes is allowed only if your cursor is at an address + that does not contain an instruction. If you attempt to change a byte + of an instruction, an "editing not allowed" message is displayed in the + status area of the tool.

-

Changed bytes are rendered in red. - This color can be changed via the Byte Viewer Edit Options by double-clicking on - the Edit Color - field.

+

Changed bytes are rendered in red. + This color can be changed via the + Byte Viewer Edit Options + by double-clicking on the Edit Color + field.

-

Undo the edit by hitting the Undo button ( ) on +

Undo the edit by hitting the Undo button () on the tool. The byte reverts to its original value. Redo your edit by - hitting the Redo button ( ).

+ hitting the Redo button ().

-

To turn off byte editing, click the - Enable/Disable - Edit toolbar button - so that it no longer appears pushed-in.

-

If - you have two Byte Viewers running, you can connect +

To turn off byte editing, click the Enable/Disable Edit toolbar button + so that it no longer appears pushed-in.

+ +

If you have two Byte Viewers running, you can + connect the two tools for the "Byte Block Edit" event so that when you make - changes - in one Byte Viewer, the other will reflect those changes in red.

+ changes in one Byte Viewer, the other will reflect those changes in red.

Cursor Colors

@@ -222,124 +223,172 @@ color="#000000">black. (Cursor colors can be changed via the Options dialog) If the byte editing is enabled and the view that is in focus supports - editing, - then the cursor is red. 

+ editing, then the cursor is red.

-
-

Byte Viewer Options:

-

The Byte - Viewer Options dialog can be used to add and remove views, set the - Alignment Address, set the - number of bytes per line, - and set the group size to be - used by the hex view.  - To launch the Byte Viewer Options dialog, - press the icon on the Byte Viewer toolbar.

+

Byte Viewer Options

+
+

The Byte Viewer Options dialog can be + used to add and remove views, set the alignment offset, set the number + of bytes per line, and set the group size to be used by the hex + view.

+

To launch the Byte Viewer Options dialog, + press the icon on the Byte Viewer toolbar.

+
-

+

-

Alignment Address  

-
-

The alignment address specifies what address should appear in - column 0.  Any address can be specified, but the address will be - normalized to be near the program's minimum address. This enables you to - view bytes in an offcut manner and to identify patterns in the bytes. - Changing the alignment address affects the offset, - which is the column that would display the bytes for address 0 if it - existed. - The offset is affected by both the alignment address and the bytes per - line. The offset is displayed as a label below the scroll - pane containing the views.

-

Sometimes - you might see a byte pattern such that you want all the bytes - to line up in the first column of the display. Consider the cursor - position in the image below. If you want to see the fourth column of - bytes (values of 00) to appear in the first column, you would enter an - alignment address of 0040b003, as indicated by your cursor position.

-

-

The result of setting the alignment address to 0040b003 - is shown below. The calculated offset is 13, the number of bytes added - to each memory block to create a new alignment. The first line of the - display shows the "remainder" bytes of 16 (bytes per line) divided by - 13, the offset. If you were to put your cursor on the starting byte of - the first line, you would see that your insertion point is 0040b000, in - this example.

-

-
- -

Set Bytes Per Line

-
-

The bytes per line indicates how many bytes are displayed in one - line in a view. The default value is 16.

-

- All formats shown must be able to support the new value. - For example,  since the HexInteger and Integer  formats show +

Bytes Per Line

+
+

The bytes per line value indicates how many bytes are displayed in one + line in a view. The default value is 16.

+ +

+ All formats shown must be able to support the new value. + For example, since the HexInteger and Integer formats show bytes in groups of four, the bytes per line must be a multiple of four. If a selected format cannot support a value for the bytes per line, an - error message will appear and the OK - button will be disabled.

-
- -

Set Group Size

-
-

The group size is the number of bytes that the Hex view shows as + error message will appear and the OK button will be disabled.

+
+ +

+ Alignment Offset

+
+

The alignment offset controls which bytes should appear in column 0. This enables + you to view bytes in an offcut manner and to identify patterns in the bytes. + The offset is displayed as a label below the scroll pane containing the views.

+ +

Sometimes + you might see a byte pattern such that you want all the bytes + to line up in the first column of the display. Consider the cursor position in the + image below:

+ +

+ +

If you want the fourth column of bytes (values of 00) to appear in + the first column, you need to decrease the alignment offset by 3, either by using + the Shift Bytes Left popup action (or hitting Ctrl+Comma) a few times, + or by changing the alignment offset value in the configuration dialog.

+ +

The result of setting the alignment offset is shown below.

+ +

+
+ +

Hex Group Size

+
+

The group size is the number of bytes that the Hex view shows as a "unit." For example, a group size of two means to show two bytes - grouped together with no spaces.

-
- -

View Selection

-
-

Each potential view is listed as a checkbox.  Select the - checkboxes corresponding to the views to be shown.  Red text + grouped together with no spaces.

+
+ +

Charset

+
+

This specifies how bytes are converted into characters. Typically + US-ASCII or UTF-8 are good choices, but other charsets + that are installed in your Java JVM will be available also.

+ +

Character encoding schemes that use more than 1 byte to encode a + character are supported, but can produce incorrect values interlaced + with the actual text characters that should be there, as each byte is treated + as the starting position of a character, even if it was already 'used' by + a previous multi-byte character.

+ +

For instance, if a sequence of UTF-8 characters are being displayed, and + some characters take 1 byte while some take 2 or 3, the positions in the + Chars display grid that are not valid starting locations may display + garbage, or just a "." dot indicating the byte could not be decoded.

+ +

Character encoding schemes that rely on escape sequences to shift between + code pages will not produce useful values in the Chars display grid as + each byte (and some number of following bytes) are treated as a unique string + to decode and will not include any previous escape sequences that would be needed + to correctly decode the bytes in question.

+ +

The available charsets are listed in a dialog that is displayed + when clicking the "..." browse button. You can filter by name, sizes, + supported scripts, etc by using the standard table filter mechanisms.

+
+ +

Compact Char Width

+
+

This option toggles between wide and narrow display of characters. If + displaying ASCII or other Latin alphabet based text, the narrow option should + be sufficient to be able to see each character.

+ +

If displaying data that has non-Latin alphabets (scripts) the wide option + will prevent characters from overwriting their neighbors.

+
+ +

Use Char Alignment

+
+

This option will only display characters for fixed-width multi-byte character + sets (such as UTF-16, UTF-32) starting at aligned offsets, avoiding converting + bytes sequences that are not aligned with the charset alignment size.

+ +

Use the alignment offset option to control + where the starting offset for aligned characters is located.

+ +

NOTE: UTF-16 is not technically a fixed-width character encoding, as a + single character may need 2 or 4 bytes to be encoded. However, it is + self-synchronizing and the 2nd half of a 4 byte sequence should be ignored and + rendered as a "." dot.

+ +

NOTE: An align-able charset needs to be marked with its alignment size in + the charset_info.json configuration file, and by default only UTF-16 and + UTF-32 have these values.

+
+ +

View Selection

+
+

Each potential view is listed as a checkbox. Select the + checkboxes corresponding to the views to be shown. Red text indicates a view cannot be displayed since it doesn't support the specified number of bytes per line.

-
+

Reorder / Resize Views

The various views in the ByteViewer can be reordered by dragging the view header to the left or right of its current position. The view - positions are swapped.

+ positions are swapped.

+

The width of each view can also be changed by dragging the separator bars in the view header to the left or right. This will resize the view that is to - the left of the separator bar.

+ the left of the separator bar.

Writing Your Own Format Plugin

-

To supply your own format to be added to the list of views +

To supply your own format to be added to the list of views displayed in the Byte Viewer,

    -
  1. Write an implementation of the ghidra.app.plugin.core.format.DataFormatModel interface, - which determines the format of how the bytes should be - represented. 
  2. -
  3. Edit your  Plugin +
  4. Write an implementation of the ghidra.app.plugin.core.format.DataFormatModel interface, + which determines the format of how the bytes should be represented.
  5. +
  6. Edit your Plugin path to include your class files if you are running Ghidra in production mode versus development mode; in development mode, you will have - to add your class files to your classpath in your development - environment.
  7. -
  8. Restart Ghidra.
  9. -
+ to add your class files to your classpath in your development environment. +
  • Restart Ghidra.
  • +
    -

    -

    Provided by: Byte Viewer Plugin

    -

    Related Topics:

    +

    Provided by: Byte Viewer Plugin

    +

    Related Topics:

    +

    + diff --git a/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/images/ByteViewer.png b/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/images/ByteViewer.png index 010df22066885ac2e8ac91cc8364b0cc119b69dc..fd9518a7400c787652954eb93760d9e5e9719337 100644 GIT binary patch literal 25034 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 cAXlVWV0+i^YIRF3v literal 20692 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 WCV0xrQ)6&j;TA_*XB#@v@9KXW6V$B$ diff --git a/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/images/ByteViewerExample.png b/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/images/ByteViewerExample.png index 34564148ed554c54db32dbe4a7f83e9c227c55d6..7e9c1b6985e4bfc09f2169626d50d8c26072f909 100644 GIT binary patch literal 24838 zcmd432UHVUw>7E<1(l|P^yU#ONRb}e!6=9zh;&G(0#ZV6f`Ed8bOq^6Kzf(nJ1AWs zfT0S65(q^IB@lQ!px-&){qJAKefJ;l4TfVN1GCrOdzHE7np^+JN^+Fvn9iLzae`9* z!M!IZPMq8Xe|XQ3f}a%g3YeZaVbCpqPe#pE51Vkdg0{8tpjbk7wKldw_B4TO+Ph*J znOR{dSjpsXcjnc=_O|T?eeidFZ$p6^ZgC8b3&V;Quqg)#J{R2-J|qawOqQ#W|8ErvtOiyC5LNf z=;(>Sur35!rv}P6$En~egEFUC$DCeUoE{gvOK>SKM<$$@6i z-EZTr{N!+Ngwu&}&@8N~<|7xrrXYWLuMQ8d-$tsRCLFAP#ZaGi!qm$rkSS^mOYq-! zdX*TTl7D+4RQZwm@Fhn=Ll(w2BO{wRDKWk3z#+Ektea^pM&t3e%mDIsuXTT(Pe#rt zY42k%H*4Q2RyR*yeCNBEY&jX1earBb7l!FoOK-F>1-v~^T<9Hp6^aLIc|ATJZ2i18 zzTH8FSv`$of#~91IF-45{ODC9tSh;LVnJGtT$9|#v*5lkJ0Mg0;vCdhz1iF?^D!Gk z63BAvM2yIbM7Nrc2Uq5DPWFv%AZboZ3faG66150VWUKv)Q2f!Xja$vO}XiHkVYr>hntpVBb6ZIQ*` zuL{Vx)3rh-G1+ldij=(1T1reXSo2Qg=s;A?`_sX(MX=zQ%Da}!xtAMj)4~6V{9^$NJR=eS8Lgo{bKv(_d;~Ewd4sr@j%Dr%E1Kph~Wps~UJtr^dc^C`^3`+3y`$F!3?6;QL1n z+7f##X=(OHrRxc zzNZkyqt5ObZCxEq$E%lnaIjq;wa`e$&b@2>q%}*QrFL(n8WUrds1ho<745e9O-^B2 zl4p!LBJgl?i!a`*^zKI)pUo52OL^rJ4-pu0I_}54lH2jZt7L4he^IiCKBi>g`-{B5eQP<( zn73{xCH9WVL-}3N0M+^%ja_@2i>XVMo&>zwbtd!jRP%v!i{*+UT+M2Prk+Oi@@Ivw zE{HU^8Ozl>jt*t!XA)nLN|V=>bw5z6vx1Vf^}S-K0$*-8O37y!O2dVUiz1{4n*++0 z#U7ZfjDXc845sA!cXY~NwTlh1-pdO&8?KDW(c`9HyF-6lc8;W!D|Vh zG#4xIzS`T^QO!7!%`aC%2gCWDUJUabn_)$!q;>64?5CxG-j&*IJ*Vw)7t8OH)T$`# zi02*_*_cPAR*!cQcUpOFmZiW``=AsuYfw6%Bp&f%IMOnK&(d`jTRy30gmDc$vg@;K z40F{Y%JokU52h`JOxFD{s8<7lzHCoDl0%x) z^M}%{C?vo5-rWzrI~uRbXUy-&zo{ea7+uM~`N@>3l9ce#+m}ty2_|oRi;3iLq{x`& zu#76)bK3uUNwTq+Y};5co7=+W+lo#zd&?c)G~)61vos6zEGBB6TWl_TbDKMH{c^9= zUT&;dv2EA*bF;78&9r9XkGCbaMxfZTQEQ9&1Q(4*vBD~ctB2TZCCIK9#$z=|U^E9& zHZB_nsTEqno8 z>Rz-QTqRTFs*_%xQ^CYFX@B`~a-$Cab@A#ocDYdQD`t7B&E(h55Pc&U*{54-k(QAL zzoR+-cr{QYICzHzy?k=y>hMSQ4~T`fLBp1gLLp@J_jHiN#)1nRU&}2P&NcV2R z$ae^0qpkOvT{p`^#6shCS&UDGul45SxNLTB-M(ynBA-_(2FCZ*pr$*iJsZjHsp56@ z6?&>=s!nr0Cv8c3Kr^maNh_HOgLmei`0-;KxtYekzYM)dKfDSn~Ea7T56Fsl?B26r4j;ryGo{T0p%#Zk3tINJ70f^WMD4IY6TN3}=0= zbE{fP;qMamHVTlL)I%1{bWnO|K}m9TpK-F26P3Y&D}uU#r$}bltX-thy=(BA@+kUrQ5^X|pNmG;L;-i7$dcif;dF~X{*d3e?O7_T z)QgI$nGo^8w->HF?C`zWQ=#f$#x7}mKC~uEt zR83Am30p2+kFh-veF8pLV2~G;m`f$Od>)BL@%U4+o{OzgVG;1V$mn))nhDaCjYFg& zuQ&w`?mTDSG^>B^_*od<(zQ(*te<~?|5+3EQ&v(hKBuiqyTB7=oC_)0dw?r&(ae^5 zljBL#{S99)1APs}(hmaESA^(B^yiLGtI&H$0))=5{~n!81em^@Hfq_Gdng z+so(^uuAsBs3QFA^lHKF$E&2KmtjjK#)eKydt1{FpkAK}^JeDqHLs5gDJ8%%H(u{D zZ#pwW^|#lfPWvRh`_i{g2)^J;)$GCuDPtbMYi=gMDyBV^Yb7o7+}3L&!0k*4M0tE# z@j@gDZzjGuG~?OR(vN8FBMZ@fB6@T00bG}FZ1Pgj^W}Iai;)lN#IVz2fV5Q?<~Tfl z1a%>lS@ZUu@ue~rmbErzvx17GhnU3S8lGX%Z*D3PtZF5!(=Y_oIb0uCCvyBSK5ezo zYZo1b+k_0J%29iS>%j#4rmfoRZj;M;s*oY=>phS^*sWw=EUZ+ z2Rz3CM8p>$d6g_PcxrYo;=-?lKJMOnmhP!3diH_w!g-}CZ{LvZEtjSbBYn;J9oTJ- zjy{CJ9~ol=FUq1D8Y!qr?hq|5%fD3FfV{f%8q7yy4Q> z@Z9z7MJD-nX|xl>CRR6PB&F>A@f+k0o0eT{@wkTS`^I(i5lsg8t*~hqpHWiV&qfo* zd)?fx{RO(VVNQW}Hm@#84Uk%;kM&}9^AEZh@WYcYazbuLJHw#TGY9_o0H$*Sj*69` z(UlsVCJWIf6`1v#tv9z7&dJtP+1`DcKE}%x&6hu+@9OgA;IPAcJ?`cwPTB<|i<-W%X)uEDrH_WQ-Yo$8uf)W`M!1HNVd%VGxHp zZvygKMa*)e2pwI1#Sp!8Gws$s@{osN$ks!MQouRF&)|dbPTpvXa=lGTozBGCDlf80 zC@EWkEe`gXB1&m*^pqsLeiC;}TaqE^Ns&%NX60I!yX(~)%T6O^hU;`4P2O4W;`;nU-xHWzu zqtZ_!t*LxcD{7rrh4Vj+Nv~u7@Iu-j)Plag&}Sa6E%f<*LPJEKAj+FVk4iM`=sjVYM~Egt)3cLvL^2&Ad+sn#g2vWcXL}Ql$%LE?hVq*_qn+LRCSmEXa=QmHLbqzW@W1%S>Sl{0VVs$)J9}GfrK`=Rsj=7kaF|8NZOe@ymR{!Nad^)|q|AS& zkb}+J z&*qSPo6PtgRp|Bn0oCR0L(43;*$6b|`}-XAsEG|^QX=tTTxAqR4>GXn~Z*nMWavJQJ%4`ffVbliC~g1fi0CqVip&)KfN z84Q57xJlR1q~WUNn=a2ItBFmN6NRLrp;|{@vOOhho7F+S#EzLIpQD2@JUaPGWLolJ z@-Pc-y21(bO;hAiHTA9NU7^zgs}o*IUtI=n^*D2!?c`M^q*-{$x+7OMoiUqRSfnQz zYp&;JD*GZ?Ps5rrWRVlOi|NT8FHq8~m)hWVdeS%&DmKU9G-Bu%!OhzP8MsxJY&G?$yXRG_QiB zjXp(|+LW16wT_2;yr!I(E6@%1BH&P8wr@p?9@MW?Ep5UVm!F<}Slm+)V6yOM{c*Rg zCkZWh&1B!VEmJpGz%=^p-4ZRdH<{=~t$obp$$|IGIeinM{>g#IU-!!5eR+1V{Lq~uCYNadG55K7xCl3S zc;LeApFDqFUp#^1%-zIwC#%xn(X0HMEYp@5;J3aA)P>}g*;Nyle}Q%M<~Z}bZMX<2 zY#r6o=kT^5LFxRd%e_00GE^Hj43Hb^r2}WZIc6F4+cicj;1{92m+PD%GUs1Q%q)#q zFqbP-@;iQFacndSzVL7BrDZeYnQdy4DW8uN8aijPb(Lu6@)HfP2rV?`T^5k}LUL?| zniI5st%)3Yu(w*kQ!(|2xEi(agF-FimSLsqgKUQFdLw4u@5o-T>LO;Gv$B0dC`U}= z5Q8P=1A{}lHhEIJv*AWo2m8C2!e%PfQABw{73e52loz>uhXMVVi86_T54zOv!3p0n z$Q&fE*RDb-QbRY3gi7?UV0P)c9YUiISq@=qR2fm!N@p?Yv8}Ewwfi(z3uR*Z3YM<^Io3B#p9!%Kn6^vSgYx&1;M4;{#9^+B``@npuVz#SCY}+Xr~sG;r7s zJQB$QN%rx$Z8If~^ekQTk^)cVJ!(#0haNFG+|sbJ$HDGA29PBxfNe40F9SwrU$`Qh z35b@v-SJ|oULs1|*gM4yM~5If(e%`o^}4?2ZW`prYns8Og8F<61~aJag;yoSaH!E7 z-%WOQGLeQIUOJ&HO(^lS+4&ag`AOJn-4i z$vxXb!oP-J&tX5@Y1eU>>ndvYer)&ab}f7lhU*X1;V;%(aZ=4{yW=jCdHQkYIO0V< z)k$b=B4uE>^_t7OHIRNd?vF#PjaQCoZ(?4?o@^*v9@$6HqLR+8KY$lLc7M%k)7T4Z zKf4mu=y9!}4xgQvV2R9*z%Li~2B1$RGabA46C?#|ecBI~ISyAFPh$a*x<(2t^Q0EH z)l2Pi>jcR5m;;W|wL4ZwwO3+8k$(&DJ(+XeaZAi2d<_zZ`x`|@=20=HK{O<9oe`h# zPO1!{*>_UtO_U6rY(9Hn-eg+j7=OD&-{)cRkHsTu+5CC}k2b|ftRx{2w%aY|@Wys(Fu~jb{(BeY{xuHS9 zK{KEt$C{=jiBlsmJb$WG{@rO5fYB!(YrhKR|!+@yhUwlgGnjp;R zYpL(FGNz4Nos?edb}bY(b22DI7%nL&ps5rI~bic~PJ`MlLsO)nudseiN@NR3w5jj2uv4*!S`p8F}g?X?QIvaA{EC)1OR;lGM ziv2p1Zq6%kBql}+`d)-m)bsfST!@H)oN@vrqK!gR?|It9`r~FW7VaK+ zJQvRZcgL2Za*I}bAxpWBp$($;purkt_iNgMqKpUBU3``*f)%vb6|1^3rFv0s8ap1v z2|v32rRku4Ati7yM0|yz>M$%Q57_{Uz#ab0s|t||B&T-2+G!9#R?Z>ksrajQvtL1w zG|^*A+o_GwY+*PG?yOy8d@rK;WS!&U69Q9AY#GXe3x4@!V}pbMlUXdoY+Eq1T<86~ zUa~O9Dk00Eg>X8~=E9{{GBdBp*!l|Aa}iIg@6P4ifbE@&_rxz^`k>TEk_$>7NGX}r zf-2uyIB1ahTI!|@wHtWc>UO^Noo<%v<@C#w4$C7&ko5~)*Q4u1x}BM9yOt~G6Svx# z=!^^Px#yZnCyVO&-cd8&xtFJ3uZLS1*UfM$b&!Ak$}^;Fe>#wNbMO5XISiL-v*h+z zYLbfA&#%|pm3W$e9aBTdXvK&JU-z9wr zdkkw`gA|ZInOs0+6ZGZeG9~jdXg5GpjGUBo+kC7EcZxu{E#={2cADOhBSEoB@4Xh> z_uLJKx!>8UtzRnNzi1ojwb3oIl=WBFgDGy5f%1~!l!&SZ&q&=Lq7S+@ub^m1uA$yt zK+fe-Q4E*e%j>eV+3tgO%w*Nn>2OoEBr|N2ZA7#G`62py>0`zW5D0%h#XFwCm&NzvFBFXs zwkHepnSZ$OC<+)t!XrpV%;D9}$OQ7qRzdP~&AzX;!YfVcJQf~O=gS8!AC^eAx;CA? zp6O3>9W%W3@l`RZo!zTuM~b4XpsC?xD8i&rI5Zo2v8$}`C?Kj6?8;0!KHPM3^8yrd zYi~+J9VJgO7g<%tFB90Bbx9AD3i}p>_{FSEwL^&!!6FY3~m%qvI_o2Skcs8G~SMXoiU?gG@lHjp0I_AmKiNWAVC^Z-{*m(y^1hi+IUrwKt);tMlnmwfhW?*x530^ znwFoGa>JcDf5P^iA%y#EqsN;Z8J4aJos$)dnP4tbl+FQ1u&I;LtBg+T=+ov)ML3I} z9HoO@x{R|PRR2V2nm`;D5SV0NpQP-(VJ3c3K0}2z*(KddglW&z`@3KUZ)%O5 z?XQ{svw?d%!DZsxHE;1yo0@f5$+v$j^9Eg=s}0N~yqg;d%632R9%Ryj^^uQU#B8Ww zr@bTgc4k7I0uamYe5PFtl0H{`*_!*l6-M{mP5e0%Dy^q~vfcl5kvJ)rpA5O?@byI3 zl&Zz_xJ|V{e$!<>mban`l0Uy(!Ie-sTdRqcNwGiZUtANv=sDlI$(Nq6QoUMdL{ZOW z$ah!L&Y5|;JevdTf0DM^Wi>y_Plf>z(#S!T+pWzCyN4jxW%t2l367qHje1X>TGySl zuEwTsD}&lmVsK5xAkKD!+WJ&eg!)>~e97q9=*G_sJ`u)6d?{P*Q9STXyU?wcZkUSZ zJSmH1g*>%LYqCUo1DG5&l1q>NI7ib?gTi%-OqK)^b6%{mK=z8vGXK#1FewI?m8$~wVOXk{B|S9MaN%zAjQbHJzET(Z3ld*>v3dlFTeg2g;@7K3kT`Tl z>K7yqS*tyQ#1|%iL*ff{E=Q1fdhZAlgNod=eN>Up^!>cYRY4xDBr|?eu683s!P%lPN8Qg&IJI)$hNvRs z8)D%yU3liod8WZf1mtZ|kXduJ_E${mr-07bO274T?uyhmFdKv0NR*tS zu4@R-oo?xGLP2huM}6&xP<4?IJ*Oy^OI?8T zU|~#dBkOD~u!jO=8W%yo(9zYWov2Gkl3!D$g6?mCctUe|6~dgW3Y~+)=c9jgs3w$Q zwRnun3R2GGRNEB3KygRX4Go#gS1W4pAjY+McECE%8;&tRq4AqiC)#|(O|uv~d838= z8OfE34~|xOf}olC;GbM9QO<38rZb0Gy(eOrrEmsX4A}hfFW;hz=9AhQj(hX;m9a&F&3VR4C(_kWQwB;Wg?JHWVF?+|!4uN=T)&0oC z?fw;}G;g8FEZ%EJYuG!&lYxa*2EDS>zu$=I!BhI+Zsz)+yG)6@pYukLTUwIlxT8#D zBNS9!C?@iWW~7{1+8{lyao((4Du`}dgaXQ>Y6}`Rh86|~@wMW^Q>#4EzYoHfY$J}8 ztTCangRzigxn@qKG6$P=Gu%eB2cP|DOsO3u^fqB$l**9IRSYrTVL}1h3%HQ6Vo309 z#M7obrrMhdXh|XH$gf!FYUx%RIFXr0o=xevnkU|Q8e~zG*SO~-f&5xxbPkOn5t(JC z6=heu?|k=s5LPV#kZ^dAf?fNNg&}ATV;CULAGmsN2Nu;-<+9kKh1x@$(D|g!?IOoF zV<*mPsR`9eoDqG^I%Pf@g1$qV@6q};yE2$)xj zI*~2#8dgqeSGQcw?Pb;qYV0VACLb;eE6GDNm0iXD&rGpJR|W^1E-FaZ5{n!ji>PM$ zI8*(TvKU+sO3AF;a!jJ#5SYR7Xta?V+|S8TO}nC{~3O5bZ!!Hwg9wU z$r)W~QpB;<18SI_gX15XD;AM9(Eup*J69Q0U;FC7B0F-Ds=ti2)1+%l9kwjhdgLO@ zp@s3^DI%5QT2l;0n^3Whm_+B8e0%g@0@~XQd+|+9FUh|gB)=@g$5OYSCmicnQQ673 zbrg}}Wg6#>Bl3-J`~N0AfrjDC5n(*xILjfPAWk&MT3UqZ5(Nej{Dw}-qfr!wP6gNG+lDL*h)&E1=@joY`ZxwF z(^gr;UA-!wVeDjBB*?)`N4mc_kg>GwI;|QfjL5Zl(`>Jw!72!QUex(rX{v*g@|SBy zW>7`5mSQo&+^=eU*l^EndDTA6_l60EBK(_M3#6I6dF8B`{Tlf*o(4i z<8Wg=T5ukF7j2OV!HX2b6{n_$Bv(dsZWH~O25R=)Oq=3Uw@F}Qw_+6Pc^?1ys-Gg& zKi6e}Cr>*=H0bF9n~>t}wPNEesw_Q=~{hCz$X{+?aH{cYdrfH~yq}Lyu+m zPZ47j5***JWQb`pq8Q3Qp-BUD2#HFsUwS$$7qz$b%*}OpAe5I{zxwt3H1e@83uom1 zmGpk9Wtc0U2)`p8IK|K`e;!XM?m6SVXt*i2vGD*T?xUmBtn~dd$=YH~Pnsjnypdvy zWc{?LV25D?HjY$$__Xqm6iM_h^z3IRbg8VF9ooip*OgecKyJ(-2MboCrnoER!><*Q zUWqeUv2^cy8&1vo5vHYQ758cM>o2_j=0XI~$4~~3Wu>3C+pxUzy`=hSW3HRaGtxOG zS6{9)kX$4G@=q7$CeZ$2dfFs_RSyQDQZOq_1B_O%hfn@-WUPfC@aS5kjEzBpveY~< zo$6>OA35rEf#36!T58Vnx1BuoLnKEj0dmQhC&gahxbIBoI?L(Kzywoq$}R(KVn)_t zU>F0JDTwvgrlP2rb8QTM4cgAU?SZ$AQl5+>!#iBBGrzy*>~zE}Wjfn6b_l5*tW;o) zX0pW%j1$Q(|9h19UrNIk6MZuh<6r3kki84h>Tx*kCVUBMg}xu3v>WzUk?+33 z0!a3+JUe6Hh@LUwhiZ9_iK08MioR1b@<6})G*Bcl1JGEcxEQgRMNrpXj?|`dHA>)` z7yA_k6t(eN6c;6^ignT-#a1zYW=R5&K!Lo?hMSuDl^C{UV3QeQ{KALjKGR ztn(jbHrie9{=UlM7d70ZuwA><4igz$j;t1Z$7PUQpSNjq-{y9)*Xid0>#_>A-06IU zmN>@5JkiM+2<5mbyJ+McME@Y`Q%R{Jd+tb5i4^)?#Uv?@D$mf2(Ngn)HoV47mCEk} z))`Bp4?)xTZwmvk2o?rlF_c+71l?n@YX=t#BF3D#ctV1i#F@bFg=0BksI;HY1AMye{9%%bij$lyQK5!-ettNOzisC(*UxoRD@i4%2gbmJ9Ud; zQu!{Qh`ZS6A|#~ayAm*f`vIz`^vOsY_Q^Hho}cTV8Enz*!MFSpjMASjk5!JmGTgP( zrkWwb#eSvv4gt1H4zCC^9k;veuL}U0Fl`F{{^g)(%X7L7DyO=#}sCI1qCXvfyP-(xA zpWiFKDv?lg+8AHd&K7qgE-oQX66^eV=FErU|7cKqW6RI=DkE-b9xgIirGwpC<F8@GqR`FblT0~!0I^2eG~D#PK09udwlCw%T{Np1RSU9*f5Eo}5XMrMVSf$kI5{{B%v_UT?^D+M|zFXEUN zb8G;2rgdICfsL!(e!lAGj4YbZ^oSD(K{J{a-GEiT;s01T_LX9|?v`0h{)+|RHB%P< zFrk5iC2|jB>CZEK*gG9Py z=H)bn9-T(Mzbjf71uIz!7dvm@=^|?&!A#0TCD=Ea=y-l zE`z;W*gM+4j^f6M!7nnkkm$VOtbU-;0W`^=ld|2h+RgT}VueRDKJ?-+{ zQtgW~#+95|(!MK^jDqIsEr5{Z|9^mx<`C9Sk}jiQ|0(19ooO1V+~lGpDNO~nl$HMZ z@b{)xd7Q*f2vmxV3g9czmpflHn-jU~u#$lv{tEdL4cw8m5EKuJK;$_NMuY%Kpb^KYsyi(&Fb#UA6WUeLxi%=jPHEQL zb>DCjiWj2h%!1F}mA-zGVvnfC>#{VVxU-2I=kXbNY$_ee&=M>kz)v$Xs7;yk}E_}Aj@D6wec@s~HB za7!zNg-DcM&vB0>?!TG1|Nf%Vezn1)TjjbwX&XyivlLKbsKq8KS(3mvueSUy2T&OeoM>_ zVN(_k`@)m8?k)teGMc?}BpxhU`B)BTFM$6|^C6s#6Oo`uqP2ovO|rmuzYc(!ZY+6= z+6DH)T3K^k2KDQ7wcdR)woJ>aNG7KY{hM(MNEW&KHzTJ`QF~$f?=BQ*anJwVrCx&C zy#5Et#^83PkP14=+Ws_lZ*vC&SzVN{sAC=K-XT0kH!^Z@?`{N&9wNJ1a zO?3cR?#sO~Bl{|YPv~CAr0=yHB|m~cWZID2&Vgsf&fm4+-s8sjc_MFtB6WRvK9R}S z&vfe5UvroLQmNRoQ#;}%IL>&6BS%n=8JhDS;ME!ca%K1ek+!C#d_Plu@#(k4&mEOk z^AgIV&aQ%~T*F&0Pm&DY%!q&TLPrA*hue9858^-WjJ!E^9?=QT6J_OLM67?}YzLT`%0vo?M+n$cnXr(=ZM8aB-`~6+od=HkR z78&gLkdqNi!}ynPfN2=_DtF+kl3hD~gV?NT&fPwlkq!SN@(hxYL)kwEqw@B=(DhvJ z_@m-EIl^h}3uh|VMg0R6Mx=xbkElSI^ZVDnj_6SSqeE0BMbEakQauQx9zQR<(+1`r z{-cX2D2_)xXb$I|4SQ5igeCL9gD(t7FwaPMju^~Taf7hcGqzr~V*kbLfD@!~vC5c&P=b&WyjZsIlD##G2xjhdv417=ttCECPKBB8;ipQnU zZblZFt6e-Ia5mqA;<&9R-t0f1>`~Js>LWu60qQU0%f!Izj;iU1eT zMOiCPla}b-6Y_vNP^(|bEGm@wzxI(wC>E^A7#5Nj(MFt^3OObl{x|e{K)dwj&2|dr z*2v;=^s{hZYW9^61UMMdd$;u6zZAvbR$@g%52g3Nr*aWWuMRU>`1bh`i~`Pi@xDAo zZ3pT((7Lola4AUHbp7n^a~UzeE@X~Hh4tp#9BnDSv-xmSgnd+?H*AZxHG-k)DvRi$ z1Afus5K5)u$La-vWAAJTviVN^fG>tjGWb-(-YV>E3WGY|_O4SvxUjYcE0bom!c%h)Jqx-;6l{U~L zsX0DgUf@Y6Sf4@Gy?G_5L=j5>>Y2XOkSEgOxIs7Ho{plEkr4{a8#3Aqc(NpqbRMqd zfyFl_U59hb$i+xq$6D-atBP&IzHXzSms`h8i=m7p%(n6{GVAoDr|y$BI!gyShqG-S zMn5>w_@#p30(TrHEmp)vPjFMP^R6<%)H(g)s&5h?gN+VkO_-zTRrNh~h+vA$0qEjc3Te-y;cpgLT9rG~A@wJT%SrIBeS5 zy3I0Z9(A@7&-@{M7`G=c$2FoMd_vRf576Fi!mPrdin}_!2_>fyBQ^+UKn02(9I~@TMtF%gkK*3 z8*=u@N#pblDU(>54n)TD*7dED1+!$C-CbUVdyFD*TG^^@J_Z5MRLIcYD3^S1M*g z20LHF2$ZV>gOa>N_ld4$(arVx9Ia3~mI$2aduJBfBp{jWb=DU;GPca#*L-gRPp%F+ z^r(>dQl=KY-x%(!~aU~#JKB0<0iXdKa}h`zs!K84%g z=v}($3`=$a+Kpb2;ypCQvgWF%CXY5EN zwmW$PlUQ9BlJVRzt=5%?o&?mhQC8=^ftd2GW%>23(wyr%3E&$9) zo3ofjB3pv=67Jl`B0s0Oxlh!DX+_&Q6*jtg7{079Ov9zU3%eQROfU)l6L;sbrk6m`C#PNo_s{m+ro8QX={a7X8C3JiGjjfQ zytE;XedZ9M|)0b-QhTifc*<4SC;wdib|ZwT-s*e$@Eb0NG3;Jj5aU%etckTz*QPp;Ny4qf^KdMhAD`V$5zSgq z^>Ffj&@e*~e?^{)f4pThbo%#&@vaWzxxN-)Vg_|$~cbM$V2h(-d=0|*ullQeMOZeG8 zwDj50@153!CEIXYJYQ>c3t;e>b_dFqNxHa|vN-GVx!XF=4Esu=3@sZh3l{H)cGhmj zT5htYnl`k&3}Ak5t9mhie-%4f|LH}vR$Otk0-yE1r{|bmdluarH=yKrNE8`8TR679 zY~}g0BkN}|Nh%;tt%-3OO)P@H5>L@bVoA`v2rW>LG2i0*K6weevwlDUSF`g^mg^L@ z<9B@jfc7fCqe?57_un3EY3;lJrwZ7@e_BR>4(vm%MNi)w%>Qm4Zg05&f^*{&G-my`hyAggjqGGd&3u@PK_Cf1dKcdaLTK(HS{5Ew3e%dx$m1G^Z)Z~Ok| zzoc=v?%1}mMD3tFO8@_$JUjX9rOe;Tvz4GIeb49Lo~CvrL&t$I4Ie~n_VY{nrHuF= zAdwW9E!cIchjS*Z3@A&h59_(-d4}wsBxCv514LibjSNr!mXMjCO3bst1|ao2_vzuE zoPm?Mk-3f>7yv!q5o%It$ph!Md8d&mB`vtY-|*-7ZMI%!U*gYB+S6|?4#?FX0Aau& zc!3NHYGoz20%h-C?Dnxj>}D`g;EJtU%EtgTkS0CM_LbXWTGTTNi9Jv#f{2BJmslvi z%0@u_+@@ZUZCb*du59CGzlLAwWVC{Y>gLCCE;>Hz4CJlT@>p-B#}mL4N1Wo@ZbbQb z`lZvPXDMnP$ekl9Jj=%HJOA?y$iU$VI#rHYo(NCt=9f>eU>R!_3r)M3JnS8N#O6PI z@E4~$l~uZ>c&AK*DsKQIPP%uHnI%+o zgx)eMq?EL-w76J4;IW4-w*Zf%HI{}8%AvRhT3WEVog21=)73H~wDI5BDTd~b1xW#- zzyX5F6-n^+p=^?a>YDJhy``=gfu+_o$(h?UQx>~(aW)#g(uXyPyA4*?--^?X4C=VI zvFhd=2|J7y76hYFJ3o(7z8gsSxgo9ZP!|h=oktAGBs~U-&6IVhMp)08>46u}u202$ z5JwIHBuP$32nQBY)w9o_hp-_HXC`)>)PrPl@S-f7dsId$~=}_W7W>pQ3 z7;D`?qF?veFS~f1j??uGm6n)eW-`OQ})oIRcyN5>P=#F^O2md-k^dK@aTy zJvz-yP3DHmWmo_$9Avq$d2qd*y`Z;drp0>%-1(2~1RzvbX}5J|+K0c6upG`$sAD=3 znG*#{`f<|h256$rv)LuTV)~rN6$;GToBS*-J9(7#Z<1G@iOf>?*}juo2U4)3oIeM0 zzK!$T39~()#(z%?3_vzpuoAUXToPwpC9h1=+?OUf&QFRN$X4g_Ai10#vr1G`bqUek zQ&GUi{M}jp8zFM6%Kz_>ApIZoXdiT;2f6Y6;=30J!t#QpD5Bk(bT+-WxQbQcq~VrJP=g{N$iy+OLqXs z#^&uq4}U3Ch~rmZi&-dHBdk-tLp@Ls6rILYW}i^E5Bp!zif*iFbr*i3GWeEqAOF7T znF1%RjD51`puM{@(4y{e+cGaEr4@++lR2zcp2z7}^a>hVUL~H*ui^9KycY``(UxNJ zl@{;Q2MpP##d>5orR~kR&rwi(`Rk3dPou#*!Kv@xfyg|_GgzA=x?Vo$N6OGB&A>;| zZ?7+ZWId9^${%tE-~YiR2NGV**oKTnp5y=+Otc&cb{!RKi4#!C8y_6i$j#f zBrftdO&Q*isLVufoyxq9y2L?^cOFG#bSfNDgz}GwS~OHN+V_rvtd-8F%k`_85t$=( zE_OGqd^CvpNBv5ETr8jd64ZqJ_4A5fd&*w~Z#2Yl;Un22ij?R8#fJboQc|4t(8Zg% z1~s;+oR4sq3evIc{oST3Qv9Aj7e7V!CBwitN(>fLedJp20Dm8%G<(17{r;C9{G|C- zwo&3QzZ(<^cw{0*%P{%XE4Mu9uLr4s%Q1>1F*U}s_d5zBP-=&;lb^hN+oz2}Q@OWm z0{>p^>fpD>k;yX;TZu00J;M4v!g$vej!v<^UEQ&Jw_i!*#g46qY9M9D(czJhuvk;v zbvJUp?&>FuI&tlFj^e&NXF6_rUtTw_?7Am4&^T1{_(*F*<9^&5z$)Uu)^COuDtafzwGWS3iV;Xa+&8U~H{C z*`~pKJliQsg#&Fz*ZIiys0d?_Ac@aaq;H?0&TAl{@$MBfJIaPY2_W_({JccNH@QL{ ziH08~14>-ieBOE^t{-tV3JTDY>kq=c5|E)s*hn1&1`)!B8K7uFm4u!z2MbTWa1e4` zGpK%W)=Kw?)6sgzX>2J(lx=h!%QmJuW;#3gp=Rj5Ma3s?=aanjn5$n!5GNZw>QR~T zYw~<#qLKdda>_|kaIcW-ibrE#XVH9YPymrHQh*X|i2F}$NaA2z6a5a-$Iaot6D=4D z69q?SI%k;GseIWYLq)P-=Vwn#0W^bK;zVcPA5)6@#M2nR1||hpt~k(0k$Vdm&7L(Y z#6JCnsk)yh-go~a(rf&54*St51bUZ({_u}o5*Q_%XokNgMS0b^L*Um@~}GO7x~ko+SNK^T3Hd*Yg;noEu8yo=b@wC6WNL zYVJ%^7hG37Sw_l^`P;bPQ^vn_GlHU-YEq7l8v8xZiIR1$lZuyGZ)zf@ovSLG8fj_k z%+fD2s_eeEGjW@EKEHVH%z{Weo4eIB*=i0e@go@s!xihEja*bXTHmY?4+bAZ-p*2) z&KSff=qTF^u#Bht^}M$)^Z(C`Ai5z*S@KL5m$lR;M|Ygau*mGy+nSWX1W~86oo9RB zc7(E=(o}yGE9fsMT$XUO(V$l}J(=Ay-m6Nad*6 zX1J0q*+{SdB3U(e`cM=+RQVsS#Q=l?_uGW6(BsSy$*Z5QdXPMQLR0MvrPTy6ON1%4 zW4zs}?VGBBAwj)%CuE4&91L+)3oB@To6~5YD00vJ^ZpWmGrx#T>{lZmi&~; zpVI3L;J>r5?&bV_8Z!TS8S?+7ss%KjTjA3dCnq5ZgxMR(5}zcC@Bfc&CiZA%^AHUc z;e;nqgYF_CGf=Ey{YGnBB>Q*LEW`D|o16*$Zcm@-5NSNB=TBFf1e(^>cqIb&K(|Qw zAxQalsad@z!O{IPt03Dcn(bYT{IjmVbHuSIk@Qv!HR~ts-wP}S)$-5EmB|?S5#KjZ zcu*o1goE=WQ|-UZw4C*iJnDRDoE#8B#uMuD*z9%Ynj7Jzkf*Ap7KFPh3w zI5|04EcU12OLwy__(-P?q5g%}eyIbg!2po*ppLwW%%DsrRgf2R*Ue zeGeb;<-szt1(K)B6a^XFVehIL2cGPPVZgO3>d=DyC^BsS`4NjMwbZmZ$1+2DQT6f? z3!d0hkz7>(eO=IRMFQy1!8j+!QEJm3OrVdXb4H?2xpQAR2X#E3(i?(h{-5TqJRZum z@3*g(N@2)Sr${18$bLHVIK<$n?0Zau5weuEIuWuQ`(EK>Ut$_-_LxG*&d?-HM=~bc zU?#l3d-Oi%yr1XuzVG?(dH(AUpYH3r?)$o~-*@?a$CU4(h6%@Q-Eq5cg;#q;)1T0+ zZ2`N1#Ut;DL%tf7tFB~%mTTTH-w7lRH>Uq1z*Rl+qa#~aDUv>J3T zTx=22Rs|l*cp}ZJDr`UI%S!d}_B%hqH&W4bSY*@*YPq-7-E*Vg6cZ4bqw2@N(sZQzLEJJxQ|`&{2xXy>g;3(OP)d1E{{+vSJ^w!E zaC zwUIYc=GL8dY;ML?-L`GuLd%h^#H5xHVmIlmE~ME0Iqq>fN=baE`X&Y<$~HbQg^SWT zPf$Heyx;5_LiyRhiguB;Y24z~E|z;ZF0>!$)k14-oaH4d>V)=MtbsT+tkbFL=M zP4oqnE;+pV_)aO{%|sBJ!zNLLY*ke6M^XJW&N_QyR4b-&ci8Fg4Gr5~lP&okZj0_? zyEJX%Mv%)+B=sG*st#yGdY4S@$X=YAX3h1tS^XV5fRob(xWihcO%t&&4xA;cJ<@CU zI6}F$sx05?d2*o)v2K9aYx&0SHCZ4iWy|*MG6tFBHd3Vx>%P)$qobNoxgRSE>{Y*H z&t{R?;_`qEmDRUGhbB89Y`mRbh91mMGIjp*u-q)DLUvQ26G3}Ej$}0JKA*YW^A$8$ zUuywV~-!IX3*e=DbqXaK)D;WkSSXNNY&$XR^6aFlSzN3=*8e7DN zK^!=;%Pg*vPa*mo;0vKGMd@dMr+RwV>a5Rx<((`jEa~;B-k!|9OIbNd;TZ{8I#IU) zWc(3#eOnrZ$AWtDjdJbIypeGRAO|X9GXOaQEe+FJN)8h)81rKUuD?< zOJJ4toVkvyi|l<$tRp2k6YtTOFr-vqL3mx$7fiT)sB>$SmY96Sw^!P)oKishVYlT5 z{^yp?Q4b^G3^6I#4E~eH%x;~S3ln{m2LX#m&v2(9oNRQl>G!i^v;Mx6%7Q#)F9u;Z zUmN*sfqS%LNO+8Q{s+-xg${!rel^op93>9&gLI2ByWpa^|Bu&fk(rOIOLb0a9 z)KQ{Rv`~5eR;BY0x>8mU&z~^W)|iOBUPKMkqS&Rb?N(p88@S`lWn*JZkRHZCnK6}3 zjn)9pwl^KJyi?7{gTNL3!5 zxt8!$%;j-g-AxKYTsQrGG`~WLiCRRS10SEgAD`O`dq$c&q$^-G4gZ%7&Yr=UK_NI& z{EuOQDf3&L%9IU0Mm#s)fR~56H1MNvvW_QZ3JseIq>Sa@{X7M4h($lh zUj!1*`;(^&bZa1%a`oMAVBoQGco7=@e`$NB@&5wn~rGusJeeHD8jZ*gUHrJax z$Ymb_6T-uUy7Ou(aipu`UPG3xw3djhP%{yDVOTemQc35Wv8rSGLydpA(H&W(>IIjZ ztm9-i_}Imf<5vJ}=5{4Y-KZLA!R$9t4B>DRQ!i&99ZSeoh7DY;YC1lrj(+mB@4))l zzP{f->0d&*q&$(X6x}1cD2HfK^@wkMT%P4y--|tr9R2@nW2x!+`WJ%~O!;q6fRGaM z|FuN^;(}NbKtuXi>9Qdy?U_ypWhD#Dn^y!ux(V{{c#V;DiOqZyLk+ zO17IqQV0%F4E~QlKJ^QEwaZ6Gt-~zA1niOes!Q=sAaS@04bwEU+H%@@D{&i=9X+BD z&Vr;BWCZamR+FA%v)7kOgN{s6S=mu$l9k~ zzp#k(Y%o4&tv#U*%?!{8qxx{m>L|J5eZjgA!Zm?pjI0rZzCCb)=m}p+g0W6w1_4&6 zqbEj|=XQIH5BF-8?I)sukA-v>pF~t|{6>q=`jZ9m?1{OcTy%zAtMJEfYp?YKvlgr| zWt}0RCk<2S`;B@jY|!3rJy8d~3a8b1%5*D_bw6#c+8G(^Va%GL4qnxz;b~+-;d$I# z9W6lrk67=zKZklnZg6keT9=(cs(ac1umENFZZ&iDU%Uqju1td!+en%E+~{>VK9Ndx zKrxVnF}VQ7Vr=)Zb0!8&-3`ofS^cVZELOT9nrKC(xH&q~*P)FMGW@_8;(zWD^ z#X%Fz!AP}h2=TS2+P?BVo)Bi2R4`GSk!7lqZeZ;x;e?%5BxW;r{lZHw{k#BhSFnO! z9tY6h{+eK#=uEyZL3AyZlDlU9QLl;K>^w9V8%B5{?L*3K#sJSaL#Aet0H4hfGv@0K1OsV^RHr!QT0bCa7qpX#2j~?=%M+eLdGtaCDPHt=49|XtwF8a^Z?Q*oA2j3_@;L7qS+`8g)TsJ^0&psL*xmaU zDE2(|-hVX4t8^V=rCSUyH*Bo=k}b=xY1h$6l*w1FZs~geXzfzp@L&4c!pYO{fVs8Z z(ov%|Hk2S_$2yG_AlN3?RF6&etfTe&po(;eVY>3-B;zb}&UK23BkYwSZUFUc02~7K&>*lnZ z==MY{MJ7?rdnSO}e|H69cC3lNw}#C6S|y^Ht$Fo+l{16IK5Q@yYb3JbuXTEXT8k1Xi;YKfb(>L zg`A!C%hB$ro#Z6vspPI!ZL6=cy3ACp)hjCzKTabCA|};x%;%+2tTtD^j1?QYNS1{RPyeal3J8q?;JfE1r2IM4gPWNa&EP%}cWK)h;$E`>75Rl^CjO3goio;<~8!eryH@(7bczrfxcKC3$?-z#wT1 zUvM6GAgw&+KaA+*0GSaJMtkyR*>bjgihtx zVa-_T=2&wyzhqAsT4nB{T;R7AyQ6&^Q^Gl!bLL%$5JFpT8J{a^2}$5&MoE0zafgdt z_se9^Ie%o=(!K)VqL?3h2)Q?L0T=3btZylm^La-~9yswVZHLHAU|SQbUIfxJ8hp{(+dkcOn1%+uJ8B zb3a6k-TO`GCw5Et<$weC{SCW&=Kityy8voW;ZxkK7TPP<)f?DUETbBeZ^Q2!Y|++4 LUo82_=H7n+SNV{7 literal 20178 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 diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteField.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteField.java index 994a586576..b8ff7741ed 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteField.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteField.java @@ -45,19 +45,20 @@ public class ByteField extends SimpleTextField { * @param startX the starting horizontal position of the field. * @param startY the starting vertical position of the field. * @param width the width of the field. + * @param charWidth width of a single character (and the cursor that will cover the character) * @param allowCursorAtEnd if true, the cursor will be allowed at the end of the field. * @param fieldOffset the column position of the fieldFactory that generated this field. * @param index the field's index * @param hlFactory the factory used to create highlights */ - public ByteField(String text, FontMetrics fontMetrics, int startX, int width, + public ByteField(String text, FontMetrics fontMetrics, int startX, int width, int charWidth, boolean allowCursorAtEnd, int fieldOffset, BigInteger index, FieldHighlightFactory hlFactory) { super(text, fontMetrics, startX, width, allowCursorAtEnd, hlFactory); this.fieldOffset = fieldOffset; this.index = index; - this.cursorWidth = fontMetrics.charWidth('W'); + this.cursorWidth = charWidth; } @Override @@ -89,6 +90,7 @@ public class ByteField extends SimpleTextField { g.setColor(cursorColor); + // we don't use getCursorBounds() so that we can specify a fixed, full-width cursor int x = startX + metrics.stringWidth(text.substring(0, cursorLoc.col())); g.fillRect(x, -heightAbove, cursorWidth, heightAbove + heightBelow); diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerActionContext.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerActionContext.java index f6feeabc06..1cb27983f6 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerActionContext.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerActionContext.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,8 +19,25 @@ import ghidra.app.context.NavigatableActionContext; public class ByteViewerActionContext extends NavigatableActionContext { + private ByteViewerComponent activeColumn; + public ByteViewerActionContext(ProgramByteViewerComponentProvider provider) { + this(provider, null); + } + + public ByteViewerActionContext(ProgramByteViewerComponentProvider provider, + ByteViewerComponent activeColumn) { super(provider, provider); + this.activeColumn = activeColumn; + } + + @Override + public ByteViewerComponentProvider getComponentProvider() { + return (ByteViewerComponentProvider) super.getComponentProvider(); + } + + public ByteViewerComponent getActiveColumn() { + return activeColumn; } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerBGColorModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerBGColorModel.java new file mode 100644 index 0000000000..405c39f682 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerBGColorModel.java @@ -0,0 +1,58 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.byteviewer; + +import java.awt.Color; +import java.math.BigInteger; +import java.util.function.Supplier; + +import docking.widgets.fieldpanel.support.BackgroundColorModel; + +/** + * {@link BackgroundColorModel} that changes the color for the currently focused byteviewer row + */ +public class ByteViewerBGColorModel implements BackgroundColorModel { + private Color bgColor = ByteViewerComponentProvider.BG_COLOR; + private Supplier cursorLocSupplier; + + /** + * Creates new model. + * + * @param cursorLocSupplier provides the index of the byteviewer-global cursor + */ + public ByteViewerBGColorModel(Supplier cursorLocSupplier) { + this.cursorLocSupplier = cursorLocSupplier; + } + + @Override + public Color getBackgroundColor(BigInteger index) { + BigInteger cursorIndex = cursorLocSupplier.get(); + return cursorIndex.equals(index) + ? ByteViewerComponentProvider.CURRENT_LINE_COLOR + : bgColor; + } + + @Override + public Color getDefaultBackgroundColor() { + return bgColor; + } + + @Override + public void setDefaultBackgroundColor(Color c) { + bgColor = c; + } + +} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java index 0c664cf7ff..d0e4acba60 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerClipboardProvider.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -135,7 +135,7 @@ public class ByteViewerClipboardProvider extends ByteCopier @Override public boolean isValidContext(ActionContext context) { - return context.getComponentProvider() == provider; + return context.getComponentProvider() == provider && currentProgram != null; } @Override @@ -179,7 +179,7 @@ public class ByteViewerClipboardProvider extends ByteCopier @Override public boolean canPaste(DataFlavor[] availableFlavors) { - if (!pasteEnabled) { + if (!pasteEnabled || currentProgram == null) { return false; } if (availableFlavors != null) { diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java index 0bf5c2b979..d72e29cd64 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponent.java @@ -15,21 +15,30 @@ */ package ghidra.app.plugin.core.byteviewer; -import java.awt.Color; -import java.awt.FontMetrics; +import java.awt.*; import java.awt.event.*; import java.math.BigInteger; +import java.util.List; -import docking.DockingUtils; +import javax.swing.JComponent; +import javax.swing.JToolTip; + +import docking.*; +import docking.action.DockingActionIf; +import docking.actions.PopupActionProvider; import docking.widgets.EventTrigger; import docking.widgets.fieldpanel.FieldPanel; import docking.widgets.fieldpanel.Layout; import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.listener.*; import docking.widgets.fieldpanel.support.*; -import generic.theme.GColor; +import generic.theme.Gui; import ghidra.app.plugin.core.format.*; +import ghidra.app.plugin.core.hover.AbstractHoverProvider; +import ghidra.app.services.HoverService; import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; import ghidra.util.Msg; import ghidra.util.Swing; import help.Help; @@ -39,44 +48,32 @@ import help.HelpService; * FieldViewer to show data formatted according to the DataFormatModel that is passed in to the * constructor. The source of the data is an array of ByteBlocks that is managed by an IndexMap. */ -public class ByteViewerComponent extends FieldPanel implements FieldMouseListener, - FieldLocationListener, FieldSelectionListener, FieldInputListener { +public class ByteViewerComponent extends FieldPanel + implements FieldMouseListener, FieldLocationListener, FieldSelectionListener, + FieldInputListener, PopupActionProvider, ByteViewerComponentNamer { private ByteViewerPanel panel; private DataFormatModel model; private int bytesPerLine; private FieldFactory[] fieldFactories; private FontMetrics fm; + private int charWidth; private IndexMap indexMap; private ProgramByteBlockSet blockSet; - - private boolean consumeKeyStrokes; - private boolean editMode; // true if this component is in edit mode; cursor is different color. - - //@formatter:off - private Color editedTextColor = ByteViewerComponentProvider.EDITED_TEXT_COLOR; - private Color focusedEditCursorColor = ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT; - private Color unfocusedEditCursorColor = ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_EDIT; - private Color focusedNonEditCursorColor = ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT; - private Color unfocusedNonEditCursorColor = ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT; - private Color currentCursorLineColor = ByteViewerComponentProvider.CURRENT_LINE_COLOR; - //@formatter:on - private ByteViewerLayoutModel layoutModel; private boolean doingRefresh; private boolean doingEdit; private boolean updatingIndexMap; - private Runnable updateColorRunner; private boolean indexUpdate = true; private FieldLocation lastFieldLoc; private ByteViewerHighlighter highlightProvider = new ByteViewerHighlighter(); - private int highlightButton = MouseEvent.BUTTON2; private FieldSelectionListener liveSelectionListener = (selection, trigger) -> { ByteBlockSelection sel = processFieldSelection(selection); panel.updateLiveSelection(ByteViewerComponent.this, sel); }; + private ByteViewerHoverProvider byteViewerHoverProvider; /** * Constructor @@ -85,25 +82,28 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene * @param layoutModel the layout model for this component * @param model data format model that knows how the data should be displayed * @param bytesPerLine number of bytes displayed in a row - * @param fm the font metrics used for drawing */ protected ByteViewerComponent(ByteViewerPanel vpanel, ByteViewerLayoutModel layoutModel, - DataFormatModel model, int bytesPerLine, FontMetrics fm) { + DataFormatModel model, int bytesPerLine) { super(layoutModel, "Byte Viewer"); setFieldDescriptionProvider((l, f) -> getFieldDescription(l, f)); this.panel = vpanel; this.model = model; this.bytesPerLine = bytesPerLine; - this.fm = fm; this.layoutModel = layoutModel; setName(model.getName()); getAccessibleContext().setAccessibleName("Byte Viewer " + model.getName()); initialize(); + } - // specialized line coloring - setBackgroundColorModel(new ByteViewerBackgroundColorModel()); + private boolean isEditMode() { + return panel.getEditMode(); + } + + private boolean isActiveComponent() { + return panel.getCurrentComponent() == this; } private String getFieldDescription(FieldLocation fieldLoc, Field field) { @@ -129,6 +129,19 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene return block.getLocationRepresentation(offset); } + @Override + public String getByteViewerComponentName() { + return model.getDescriptiveName(); + } + + @Override + public List getPopupActions(Tool tool, ActionContext context) { + if (model instanceof PopupActionProvider popupProvider) { + return popupProvider.getPopupActions(tool, context); + } + return null; + } + @Override public void buttonPressed(FieldLocation fieldLocation, Field field, MouseEvent mouseEvent) { if (fieldLocation == null || field == null) { @@ -139,7 +152,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene return; } - if (mouseEvent.getButton() == highlightButton) { + if (mouseEvent.getButton() == panel.getHighlightButton()) { String text = field.getText(); if (text.equals(highlightProvider.getText())) { highlightProvider.setText(null); @@ -154,6 +167,10 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene mouseEvent.getButton() == MouseEvent.BUTTON1) { fieldLocationChanged(fieldLocation, field, true, false); } + else if (!isActiveComponent()) { + // there was a click but the view wasn't the active view + fieldLocationChanged(fieldLocation, field, false, true); + } } @Override @@ -164,22 +181,30 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene private void fieldLocationChanged(FieldLocation loc, Field field, boolean isAltDown, boolean setCurrentView) { // tell the panel that the location has changed - // translate location if (doingRefresh || doingEdit || loc == null || indexMap == null || field == null || updatingIndexMap) { return; } - if (!(field instanceof ByteField) || (!isAltDown && loc.equals(lastFieldLoc))) { - return; + if (indexMap.isBlockSeparatorIndex(loc.getIndex())) { + // special handling for non-byte mapped lines to insure other columns remain in sync + panel.setCurrentNonMappedIndex(loc.getIndex(), this); + } + if (lastFieldLoc == null || !loc.getIndex().equals(lastFieldLoc.getIndex())) { + // needed because the index column doesn't have a cursor that causes it to always + // be repainted and have the ability to repaint the current line background + panel.updateIndexColumnCurrentLine(); } if (setCurrentView) { //Set this component as the current view in the panel - panel.setCurrentView(ByteViewerComponent.this); + panel.setCurrentView(this); + } + if (!(field instanceof ByteField) || (!isAltDown && loc.equals(lastFieldLoc))) { + return; } // Update later because the field panel listener is called after this one, and sets the // colors incorrectly - Swing.runLater(updateColorRunner); + Swing.runLater(() -> updateColors()); lastFieldLoc = loc; @@ -229,7 +254,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene Field field) { panel.setStatusMessage(""); - if (!consumeKeyStrokes) { + if (!isEditMode()) { return; } @@ -245,7 +270,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene return; } - if (!model.isEditable()) { + if (!(model instanceof MutableDataFormatModel mutableModel)) { panel.setStatusMessage(model.getName() + " view is not editable"); ev.consume(); // we are in edit mode-don't let the event go through return; @@ -295,7 +320,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene } try { byte[] oldValue = getByteValue(block, offset); - boolean success = model.replaceValue(block, offset, col, c); + boolean success = mutableModel.replaceValue(block, offset, col, c); if (success) { byte[] newValue = getByteValue(block, offset); blockSet.notifyByteEditing(block, offset, oldValue, newValue); @@ -315,10 +340,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene getToolkit().beep(); } - catch (AddressOutOfBoundsException e) { - getToolkit().beep(); - } - catch (IndexOutOfBoundsException e) { + catch (AddressOutOfBoundsException | IndexOutOfBoundsException e) { getToolkit().beep(); } catch (Throwable t) { @@ -332,6 +354,23 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene } } + @Override + public void setFont(Font font) { + super.setFont(font); + fm = getFontMetrics(getFont()); + if (model != null && layoutModel != null) { + invalidateModelFields(); + } + } + + void invalidateModelFields() { + charWidth = model instanceof CursorWidthDataFormatModel cwdfm + ? cwdfm.getCursorWidth(fm) + : fm.charWidth('W'); + createFields(); // redo the fields... + layoutModel.setIndexMap(indexMap); + } + private byte[] getByteValue(ByteBlock block, BigInteger offset) { byte[] b = new byte[model.getUnitByteSize()]; try { @@ -354,40 +393,6 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene addFieldMouseListener(this); } - void setFontMetrics(FontMetrics fm) { - this.fm = fm; - createFields(); - layoutModel.setIndexMap(indexMap); - } - - void setHighlightButton(int highlightButton) { - this.highlightButton = highlightButton; - } - - void setMouseButtonHighlightColor(Color color) { - highlightProvider.setHighlightColor(color); - } - - /** - * Set the color for showing gaps in indexes. - * - * @param c the color to set - */ - void setSeparatorColor(Color c) { - for (FieldFactory fieldFactorie : fieldFactories) { - fieldFactorie.setSeparatorColor(c); - } - layoutModel.layoutChanged(); - } - - /** - * Get the color of unsaved byte changes - * @return the color of unsaved byte changes - */ - Color getEditedTextColor() { - return editedTextColor; - } - void setIndexMap(IndexMap map) { updatingIndexMap = true; indexMap = map; @@ -397,13 +402,9 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene createFields(); } - ByteBlockSet byteBlockSet = indexMap.getByteBlockSet(); - if (byteBlockSet instanceof ProgramByteBlockSet) { - blockSet = (ProgramByteBlockSet) indexMap.getByteBlockSet(); - } - else { - blockSet = null; - } + blockSet = indexMap.getByteBlockSet() instanceof ProgramByteBlockSet pbbs ? pbbs : null; + byteViewerHoverProvider + .setProgram(blockSet != null && blockSet.isValid() ? blockSet.program : null); if (indexUpdate) { layoutModel.setIndexMap(indexMap); } @@ -418,18 +419,6 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene return blockSet; } - /** - * Set the new group size - * - * @param groupSize the group size - * @throws UnsupportedOperationException if model for this view does not support groups - */ - void setGroupSize(int groupSize) { - model.setGroupSize(groupSize); - createFields(); // redo the fields... - layoutModel.setIndexMap(indexMap); - } - void setViewerSelection(ByteBlockSelection selection) { removeFieldSelectionListener(this); try { @@ -506,7 +495,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene int fieldNum = location.getFieldNum(); int row = location.getRow(); setCursorPosition(fieldIndex, fieldNum, row, column, EventTrigger.INTERNAL_ONLY); - if (panel.getCurrentComponent() == this) { + if (isActiveComponent()) { goTo(fieldIndex, fieldNum, row, column, false, EventTrigger.INTERNAL_ONLY); } @@ -599,7 +588,7 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene * Convert the cursor location to a byte block and an offset. * @return the cursor location to a byte block and an offset. */ - ByteBlockInfo getViewerCursorLocation() { + public ByteBlockInfo getViewerCursorLocation() { FieldLocation loc = getCursorLocation(); if (loc == null) { ViewerPosition vp = getViewerPosition(); @@ -636,39 +625,16 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene return model; } - /** - * Set the edit mode according to the given param if the model for this view supports editing. - * - * @param editMode true means to enable editing, and change the cursor color. - */ - void setEditMode(boolean editMode) { - consumeKeyStrokes = editMode; - if (!model.isEditable()) { - return; - } - this.editMode = editMode; - updateFocusedColor(); - udpateNonFocusedColor(); + private Color getActiveColor() { + return isEditMode() + ? ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT + : ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT; } - private void updateFocusedColor() { - if (panel.getCurrentComponent() == this) { - if (editMode) { - setFocusedCursorColor(focusedEditCursorColor); - } - else { - setFocusedCursorColor(focusedNonEditCursorColor); - } - } - } - - private void udpateNonFocusedColor() { - Color c = editMode ? unfocusedEditCursorColor : unfocusedNonEditCursorColor; - setNonFocusCursorColor(c); - } - - boolean getEditMode() { - return editMode; + void updateColors() { + setFocusedCursorColor(isActiveComponent() + ? getActiveColor() + : ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT); } /** @@ -700,28 +666,25 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene model.dispose(); // scrollPane.getViewport().removeChangeListener(this); layoutModel.dispose(); + fieldFactories = null; } - //////////////////////////////////////////////////////////////////////// - /** * Set up colors and mouse listener. */ private void initialize() { + setFont(ByteViewerComponentProvider.DEFAULT_FONT); + createFields(); setCursorOn(true); - editedTextColor = ByteViewerComponentProvider.EDITED_TEXT_COLOR; - setNonFocusCursorColor(ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT); setFocusedCursorColor(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); - updateColorRunner = () -> updateFocusedColor(); - addMouseListener(new MouseAdapter() { @Override public void mouseReleased(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON3) { + if (e.getButton() == MouseEvent.BUTTON3 && !isActiveComponent()) { // hack to make sure that a right-clicked component becomes the active // component panel.setCurrentView(ByteViewerComponent.this); @@ -729,7 +692,19 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene } }); + setBackgroundColor(ByteViewerComponentProvider.BG_COLOR); + // specialized line coloring + setBackgroundColorModel(new ByteViewerBGColorModel(() -> getCursorLocation().getIndex())); + + Gui.registerFont(this, ByteViewerComponentProvider.DEFAULT_FONT_ID); + + invalidateModelFields(); + enableHelp(); + + byteViewerHoverProvider = + new ByteViewerHoverProvider("ByteViewer" + model.getName() + "Hover"); + setHoverProvider(byteViewerHoverProvider); } /** @@ -747,15 +722,13 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene */ private void createFields() { - int fieldCount = bytesPerLine / model.getUnitByteSize(); + int fieldCount = Math.max(bytesPerLine / model.getUnitByteSize(), 1); fieldFactories = new FieldFactory[fieldCount]; - int charWidth = fm.charWidth('W'); int fieldOffset = 0; for (int i = 0; i < fieldCount; i++) { - fieldFactories[i] = - new FieldFactory(model, bytesPerLine, fieldOffset, fm, highlightProvider); + fieldFactories[i] = new FieldFactory(model, bytesPerLine, fieldOffset, charWidth, fm, + highlightProvider); fieldOffset += model.getUnitByteSize(); - fieldFactories[i].setEditColor(editedTextColor); fieldFactories[i].setIndexMap(indexMap); } layoutModel.setFactorys(fieldFactories, model, charWidth); @@ -904,56 +877,81 @@ public class ByteViewerComponent extends FieldPanel implements FieldMouseListene return result; } - private class ByteViewerBackgroundColorModel implements BackgroundColorModel { + /** + * Provides hover / tooltip popup for ByteViewer data models that implement + * {@link TooltipDataFormatModel}. + *

    + * Typically HoverProviders rely on HoverServices (individually installed via plugins) that + * produce customized data for different components. This class just hardwires everything + * together. + */ + private class ByteViewerHoverProvider extends AbstractHoverProvider implements HoverService { - private Color defaultBackgroundColor = new GColor("color.bg.byteviewer"); - - @Override - public Color getBackgroundColor(BigInteger index) { - if (indexIsInCurrentLine(index)) { - return currentCursorLineColor; - } - - return defaultBackgroundColor; - } - - private boolean indexIsInCurrentLine(BigInteger layoutIndex) { - Field currentField = getCurrentField(); - if (!(currentField instanceof ByteField)) { - // empty field - return false; - } - - ByteField currentByteField = (ByteField) currentField; - BigInteger currentIndex = currentByteField.getIndex(); - Layout layout = layoutModel.getLayout(layoutIndex); - int n = layout.getNumFields(); - for (int i = 0; i < n; i++) { - Field field = layout.getField(i); - if (!(field instanceof ByteField)) { - continue; - } - - ByteField byteField = (ByteField) field; - BigInteger fieldLayoutIndex = byteField.getIndex(); - if (fieldLayoutIndex.equals(currentIndex)) { - return true; - } - } - - return false; + public ByteViewerHoverProvider(String windowName) { + super(windowName); + addHoverService(this); } @Override - public Color getDefaultBackgroundColor() { - return defaultBackgroundColor; + protected ProgramLocation getHoverLocation(FieldLocation fieldLocation, Field field, + Rectangle fieldBounds, MouseEvent event) { + return model instanceof TooltipDataFormatModel && field instanceof ByteField + ? new ProgramLocation() + : null; } @Override - public void setDefaultBackgroundColor(Color c) { - defaultBackgroundColor = c; + public int getPriority() { + return 0; } + @Override + public boolean hoverModeSelected() { + return true; + } + + @Override + public JComponent getHoverComponent(Program unusedProgram, ProgramLocation unusedProgLoc, + FieldLocation fieldLocation, Field field) { + + if (!(field instanceof ByteField bf) || + !(model instanceof TooltipDataFormatModel ttdfm)) { + return null; + } + BigInteger index = fieldLocation.getIndex(); + ByteBlockInfo info = indexMap.getBlockInfo(index, bf.getFieldOffset()); + + if (info == null) { + return null; + } + + String ttStr = + ttdfm.getTooltip(info.getBlock(), info.getOffset(), ByteViewerComponent.this); + if (ttStr != null && !ttStr.isBlank()) { + JToolTip tt = new JToolTip(); + tt.setTipText(ttStr); + return tt; + } + return null; + } + + @Override + public void componentHidden() { + // nothing + } + + @Override + public void componentShown() { + // nothing + } + + @Override + public void scroll(int amount) { + // WARNING: unusual situation. This method signature is the same between both + // AbstractHoverProvider and the HoverService interface. + // AbstractHoverProvider calls the scroll() on the service, but when + // both calls end up at the same method, you will get a stackoverflow. + // We implement a do-nothing here that prevents that. + } } - } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentNamer.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentNamer.java new file mode 100644 index 0000000000..225df89389 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentNamer.java @@ -0,0 +1,24 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.byteviewer; + +/** + * Allows components used as a column in the byteviewer to specify a more descriptive + * name to use for the header above the format's column. + */ +public interface ByteViewerComponentNamer { + String getByteViewerComponentName(); +} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java index 184d303d45..08d73fd584 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java @@ -17,16 +17,22 @@ package ghidra.app.plugin.core.byteviewer; import static ghidra.GhidraOptions.*; -import java.awt.*; +import java.awt.Font; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; import java.math.BigInteger; import java.util.*; -import java.util.List; import javax.swing.JComponent; +import javax.swing.KeyStroke; -import docking.action.ToggleDockingAction; -import generic.theme.GColor; -import generic.theme.GIcon; +import docking.*; +import docking.action.*; +import docking.action.builder.ActionBuilder; +import docking.action.builder.ToggleActionBuilder; +import docking.actions.PopupActionProvider; +import docking.widgets.fieldpanel.support.ViewerPosition; +import generic.theme.*; import ghidra.GhidraOptions; import ghidra.GhidraOptions.CURSOR_MOUSE_BUTTON_NAMES; import ghidra.app.plugin.core.format.*; @@ -37,11 +43,12 @@ import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.PluginTool; import ghidra.util.HelpLocation; import ghidra.util.Msg; +import ghidra.util.charset.CharsetInfo; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.task.SwingUpdateManager; public abstract class ByteViewerComponentProvider extends ComponentProviderAdapter - implements OptionsChangeListener { + implements OptionsChangeListener, PopupActionProvider { protected static final String BLOCK_NUM = "Block Num"; protected static final String BLOCK_OFFSET = "Block Offset"; @@ -51,18 +58,16 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt protected static final String Y_OFFSET = "Y Offset"; private static final String VIEW_NAMES = "View Names"; private static final String VIEW_WIDTHS = "View_Widths"; - private static final String HEX_VIEW_GROUPSIZE = "Hex view groupsize"; - private static final String BYTES_PER_LINE_NAME = "Bytes Per Line"; - private static final String OFFSET_NAME = "Offset"; static final int DEFAULT_NUMBER_OF_CHARS = 8; static final String DEFAULT_FONT_ID = "font.byteviewer"; - static final int DEFAULT_BYTES_PER_LINE = 16; + static final Font DEFAULT_FONT = Gui.getFont(DEFAULT_FONT_ID); + static final String HEADER_FONT_ID = "font.byteviewer.header"; + static final Font HEADER_FONT = Gui.getFont(HEADER_FONT_ID); //@formatter:off - static final String FG = "byteviewer.color.fg"; - static final String CURSOR = "byteviewer.color.cursor"; - + static final GColor FG_COLOR = new GColor("color.fg"); + static final GColor BG_COLOR = new GColor("color.bg.byteviewer"); static final GColor SEPARATOR_COLOR = new GColor("color.fg.byteviewer.separator"); static final GColor EDITED_TEXT_COLOR = new GColor("color.fg.byteviewer.changed"); @@ -72,6 +77,8 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt static final GColor CURSOR_COLOR_UNFOCUSED_NON_EDIT = new GColor("color.cursor.byteviewer.unfocused.non.edit"); static final GColor CURRENT_LINE_COLOR = GhidraOptions.DEFAULT_CURSOR_LINE_COLOR; + static final GColor HIGHLIGHT_COLOR = new GColor("color.bg.byteviewer.highlight"); + static final GColor HIGHLIGHT_MIDDLE_MOUSE_COLOR = new GColor("color.bg.byteviewer.highlight.middle.mouse"); //@formatter:on static final String INDEX_COLUMN_NAME = "Addresses"; @@ -91,19 +98,14 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt private static final String OPTION_HIGHLIGHT_CURSOR_LINE = GhidraOptions.HIGHLIGHT_CURSOR_LINE_OPTION_NAME; private static final String OPTION_HIGHLIGHT_MIDDLE_MOUSE_NAME = "Middle Mouse Color"; - private static final GColor HIGHLIGHT_MIDDLE_MOUSE_COLOR = - new GColor("color.bg.byteviewer.highlight.middle.mouse"); protected ByteViewerPanel panel; - private int bytesPerLine; - private int offset; - private int hexGroupSize = 1; + private ByteViewerConfigOptions configOptions = new ByteViewerConfigOptions(); protected Map viewMap = new HashMap<>(); protected ToggleDockingAction editModeAction; - protected OptionsAction setOptionsAction; protected ProgramByteBlockSet blockSet; @@ -112,6 +114,9 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt protected SwingUpdateManager updateManager; private Map> dataFormatModelClassMap; + private DockingAction shiftLeftAction; + private DockingAction shiftRightAction; + private DockingAction optionsAction; protected ByteViewerComponentProvider(PluginTool tool, AbstractByteViewerPlugin plugin, String name, Class contextType) { @@ -122,8 +127,8 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt initializedDataFormatModelClassMap(); panel = newByteViewerPanel(); - bytesPerLine = DEFAULT_BYTES_PER_LINE; setIcon(new GIcon("icon.plugin.byteviewer.provider")); + setOptions(); createActions(); @@ -132,6 +137,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt addView(DEFAULT_VIEW); setWindowMenuGroup("Byte Viewer"); + tool.addPopupActionProvider(this); } protected ByteViewerPanel newByteViewerPanel() { @@ -146,12 +152,75 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt } } - private void createActions() { - editModeAction = new ToggleEditAction(this, plugin); - setOptionsAction = new OptionsAction(this, plugin); + ToggleDockingAction getEditModeAction() { + // for junit + return editModeAction; + } - addLocalAction(editModeAction); - addLocalAction(setOptionsAction); + DockingAction getShiftLeftAction() { + // for junit + return shiftLeftAction; + } + + DockingAction getShiftRightAction() { + // for junit + return shiftRightAction; + } + + DockingAction getOptionsAction() { + // for junit + return optionsAction; + } + + private void createActions() { + editModeAction = + new ToggleActionBuilder("Enable/Disable Byteviewer Editing", plugin.getName()) + .selected(false) + .description("Enable/Disable editing of bytes in Byte Viewer panels.") + .toolBarIcon(new GIcon("icon.base.edit.bytes")) + .toolBarGroup("Byteviewer") + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_E, + DockingUtils.CONTROL_KEY_MODIFIER_MASK | InputEvent.ALT_DOWN_MASK)) + .enabledWhen(ac -> blockSet != null && blockSet.isValid()) + .onAction(ac -> setEditMode(editModeAction.isSelected())) + .buildAndInstallLocal(this); + + optionsAction = new ActionBuilder("Byte Viewer Options", plugin.getName()) + .description("Set Byte Viewer Options") + .toolBarIcon(new GIcon("icon.plugin.byteviewer.options")) + .toolBarGroup("ZSettings") + .enabledWhen(ac -> blockSet != null && blockSet.isValid()) + .onAction(ac -> tool.showDialog( + new ByteViewerOptionsDialog(ByteViewerComponentProvider.this), + ByteViewerComponentProvider.this)) + .buildAndInstallLocal(this); + + shiftLeftAction = new ActionBuilder("Shift Alignment Offset Left", plugin.getName()) + .description("Shift Alignment Offset Left") + .popupMenuGroup("ByteOffsetShift") + .popupMenuPath("Shift Bytes Left") + .keyBinding("ctrl-comma") + .enabledWhen(ac -> blockSet != null && blockSet.isValid()) + .onAction(ac -> adjustOffset(-1)) + .buildAndInstallLocal(this); + + shiftRightAction = new ActionBuilder("Shift Alignment Offset Right", plugin.getName()) + .description("Shift Alignment Offset Right") + .popupMenuGroup("ByteOffsetShift") + .popupMenuPath("Shift Bytes Right") + .keyBinding("ctrl-period") + .enabledWhen(ac -> blockSet != null && blockSet.isValid()) + .onAction(ac -> adjustOffset(+1)) + .buildAndInstallLocal(this); + } + + @Override + public List getPopupActions(Tool tool, ActionContext context) { + if (context instanceof ByteViewerActionContext bvContext && + bvContext.getComponentProvider() == this) { + return bvContext.getActiveColumn().getPopupActions(tool, bvContext); + } + return null; } @Override @@ -179,12 +248,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt @Override public void optionsChanged(ToolOptions options, String optionName, Object oldValue, Object newValue) { - if (options.getName().equals("ByteViewer")) { - if (optionName.equals(OPTION_FONT)) { - setFont((Font) newValue); - } - } - else if (options.getName().equals(CATEGORY_BROWSER_FIELDS)) { + if (options.getName().equals(CATEGORY_BROWSER_FIELDS)) { if (optionName.equals(CURSOR_HIGHLIGHT_BUTTON_NAME)) { CURSOR_MOUSE_BUTTON_NAMES mouseButton = (CURSOR_MOUSE_BUTTON_NAMES) newValue; panel.setHighlightButton(mouseButton.getMouseEventID()); @@ -192,12 +256,6 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt } } - private void setFont(Font font) { - FontMetrics fm = panel.getFontMetrics(font); - panel.setFontMetrics(fm); - tool.setConfigChanged(true); - } - // Options.getStringEnum() is deprecated private void setOptions() { ToolOptions opt = tool.getOptions("ByteViewer"); @@ -238,13 +296,6 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt opt.registerOption(OPTION_HIGHLIGHT_CURSOR_LINE, true, help, "Toggles highlighting background color of line containing the cursor."); - Color separatorColor = opt.getColor(SEPARATOR_COLOR_OPTION_NAME, SEPARATOR_COLOR); - panel.setSeparatorColor(separatorColor); - - Color middleMouseColor = - opt.getColor(OPTION_HIGHLIGHT_MIDDLE_MOUSE_NAME, HIGHLIGHT_MIDDLE_MOUSE_COLOR); - panel.setMouseButtonHighlightColor(middleMouseColor); - opt.addOptionsChangeListener(this); // cursor highlight options @@ -257,20 +308,24 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt } /** - * Set the offset that is applied to each block. - * @param blockOffset the new block offset + * Set the display offset that is applied to bytes in each block. + *

    + * Changing this adjusts which byte appears first on each line of the grid. + * + * @param newOffset the new block offset (0..bytesPerLine-1) */ - void setBlockOffset(int blockOffset) { - if (blockOffset == offset) { - return; + public void setOffset(int newOffset) { + if (configOptions.calcNormalizedOffset(newOffset) != configOptions.getOffset()) { + configOptions.setOffset(newOffset); + ViewerPosition vp = panel.getViewerPosition(); + panel.updateLayoutConfigOptions(configOptions); + tool.setConfigChanged(true); + panel.setViewerPosition(vp); } - int newOffset = blockOffset; - if (newOffset > bytesPerLine) { - newOffset = newOffset % bytesPerLine; - } - this.offset = newOffset; - panel.setOffset(newOffset); - tool.setConfigChanged(true); + } + + void adjustOffset(int delta) { + setOffset(configOptions.getOffset() + delta); } ByteBlockInfo getCursorLocation() { @@ -289,48 +344,105 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt return blockSet; } - /** - * Get the number of bytes displayed in a line. - * @return the number of bytes displayed in a line - */ - int getBytesPerLine() { - return bytesPerLine; + public ByteViewerConfigOptions getConfigOptions() { + return configOptions; } - /** - * Get the offset that should be applied to each byte block. - * @return the offset that should be applied to each byte block - */ - int getOffset() { - return offset; - } + public void updateConfigOptions(ByteViewerConfigOptions newOptions, Set selectedViews) { - Color getFocusedNonEditCursorColor() { - return CURSOR_COLOR_FOCUSED_NON_EDIT; - } + boolean changed = removeDeletedViews(selectedViews); + if (!configOptions.areOptionsEqual(newOptions)) { + changed = true; - int getGroupSize() { - return hexGroupSize; - } + boolean layoutChanged = configOptions.areLayoutParamsChanged(newOptions); + boolean widthsChanged = configOptions.areDislayWidthsChanged(newOptions); - void setGroupSize(int groupSize) { - if (groupSize == hexGroupSize) { - return; - } - hexGroupSize = groupSize; - ByteViewerComponent component = viewMap.get(HexFormatModel.NAME); - if (component != null) { - component.setGroupSize(groupSize); - component.invalidate(); + configOptions = newOptions; + + for (ByteViewerComponent bvc : viewMap.values()) { + bvc.getDataModel().setByteViewerConfigOptions(configOptions); + bvc.invalidateModelFields(); + } + + if (layoutChanged || widthsChanged) { + panel.updateLayoutConfigOptions(configOptions); + } + if (widthsChanged) { + panel.resetColumnsToDefaultWidths(); + } + panel.invalidate(); + panel.validate(); panel.repaint(); } - tool.setConfigChanged(true); + + + changed |= addNewViews(selectedViews); + + if (changed) { + refreshView(); + tool.setConfigChanged(true); + } } - void setBytesPerLine(int bytesPerLine) { - if (this.bytesPerLine != bytesPerLine) { - this.bytesPerLine = bytesPerLine; - panel.setBytesPerLine(bytesPerLine); + private boolean removeDeletedViews(Set selectedViews) { + if (selectedViews == null) { + return false; + } + boolean changed = false; + for (String viewName : getCurrentViews()) { + if (!selectedViews.contains(viewName)) { + removeView(viewName, true); + changed = true; + } + } + return changed; + } + + private boolean addNewViews(Set selectedViews) { + if (selectedViews == null) { + return false; + } + boolean changed = false; + Set currentViews = getCurrentViews(); + + // add any missing views + for (String viewName : selectedViews) { + if (!currentViews.contains(viewName)) { + addView(viewName); + changed = true; + } + } + return changed; + } + + private void updateModelConfig(String modelName) { + ByteViewerComponent bvc = viewMap.get(modelName); + if (bvc != null) { + bvc.getDataModel().setByteViewerConfigOptions(configOptions); + bvc.invalidateModelFields(); + panel.repaint(); + } + } + + public void setCharsetInfo(CharsetInfo newCSI) { + CharsetInfo oldCSI = configOptions.getCharsetInfo(); + if (!oldCSI.equals(newCSI)) { + configOptions.setCharsetInfo(newCSI); + // we know only Chars format cares about this setting + updateModelConfig(CharacterFormatModel.NAME); + if (oldCSI.getAlignment() != newCSI.getAlignment()) { + panel.resetColumnsToDefaultWidths(); + } + tool.setConfigChanged(true); + } + } + + public void setCompactChars(boolean newCompactChars) { + if (configOptions.isCompactChars() != newCompactChars) { + configOptions.setCompactChars(newCompactChars); + // we know only Chars format cares about this setting, and that it will change column width + updateModelConfig(CharacterFormatModel.NAME); + panel.resetColumnsToDefaultWidths(); tool.setConfigChanged(true); } } @@ -338,9 +450,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt protected void writeConfigState(SaveState saveState) { List viewNames = panel.getViewNamesInDisplayOrder(); saveState.putStrings(VIEW_NAMES, viewNames.toArray(new String[viewNames.size()])); - saveState.putInt(HEX_VIEW_GROUPSIZE, hexGroupSize); - saveState.putInt(BYTES_PER_LINE_NAME, bytesPerLine); - saveState.putInt(OFFSET_NAME, offset); + configOptions.write(saveState); SaveState columnState = new SaveState(VIEW_WIDTHS); int indexWidth = panel.getViewWidth(INDEX_COLUMN_NAME); columnState.putInt(INDEX_COLUMN_NAME, indexWidth); @@ -352,12 +462,13 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt } protected void readConfigState(SaveState saveState) { + configOptions.read(saveState); + String[] names = saveState.getStrings(VIEW_NAMES, new String[0]); - hexGroupSize = saveState.getInt(HEX_VIEW_GROUPSIZE, 1); restoreViews(names, false); - bytesPerLine = saveState.getInt(BYTES_PER_LINE_NAME, DEFAULT_BYTES_PER_LINE); - offset = saveState.getInt(OFFSET_NAME, 0); - panel.restoreConfigState(bytesPerLine, offset); + + panel.restoreConfigState(configOptions); + SaveState viewWidths = saveState.getSaveState(VIEW_WIDTHS); if (viewWidths != null) { String[] viewNames = viewWidths.getNames(); @@ -366,7 +477,6 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt if (width > 0) { panel.setViewWidth(viewName, width); } - } } } @@ -376,7 +486,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt */ private void restoreViews(String[] viewNames, boolean updateViewPosition) { // clear existing views - for (String viewName : viewMap.keySet()) { + for (String viewName : List.copyOf(viewMap.keySet())) { removeView(viewName, false); } for (String viewName : viewNames) { @@ -400,9 +510,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt private ByteViewerComponent addView(DataFormatModel model, boolean configChanged, boolean updateViewPosition) { - if (model.getName().equals(HexFormatModel.NAME)) { - model.setGroupSize(hexGroupSize); - } + model.setByteViewerConfigOptions(configOptions); String viewName = model.getName(); ByteViewerComponent bvc = @@ -436,9 +544,12 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt protected abstract void updateLiveSelection(ByteBlockSelection selection); void dispose() { + tool.removePopupActionProvider(this); updateManager.dispose(); updateManager = null; + panel.dispose(); + if (blockSet != null) { blockSet.dispose(); } @@ -491,6 +602,12 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt return names; } + /** + * Factory, creates instances of DataFormatModel. + * + * @param formatName name + * @return new instance of the requested DataFormatModel + */ public DataFormatModel getDataFormatModel(String formatName) { Class classy = dataFormatModelClassMap.get(formatName); if (classy == null) { diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerConfigOptions.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerConfigOptions.java new file mode 100644 index 0000000000..a5c9a50bb9 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerConfigOptions.java @@ -0,0 +1,162 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.byteviewer; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import ghidra.framework.options.SaveState; +import ghidra.util.charset.CharsetInfo; +import ghidra.util.charset.CharsetInfoManager; + +/** + * Configuration values for byte viewer data models, as well as the bytes_per_line of the + * byte viewer itself. + */ +public class ByteViewerConfigOptions { + static final int DEFAULT_BYTES_PER_LINE = 16; + + private static final String HEX_VIEW_GROUPSIZE_OPTION_NAME = "Hex View Groupsize"; + private static final String CHARSET_OPTION_NAME = "Charset Name"; + private static final String COMPACTCHARS_OPTION_NAME = "Compact Chars"; + private static final String USE_CHAR_ALIGNMENT_OPTION_NAME = "Use Char Alignment"; + private static final String BYTES_PER_LINE_OPTION_NAME = "Bytes Per Line"; + private static final String OFFSET_NAME = "Offset"; + + private int bytesPerLine = DEFAULT_BYTES_PER_LINE; + private int offset; + private boolean compactChars = true; + private boolean useCharAlignment = true; + private CharsetInfo csi = CharsetInfoManager.getInstance().get(StandardCharsets.US_ASCII); + private int hexGroupSize = 1; + + public ByteViewerConfigOptions() { + // nothing + } + + @Override + public ByteViewerConfigOptions clone() { + ByteViewerConfigOptions clone = new ByteViewerConfigOptions(); + clone.bytesPerLine = bytesPerLine; + clone.compactChars = compactChars; + clone.useCharAlignment = useCharAlignment; + clone.csi = csi; + clone.hexGroupSize = hexGroupSize; + clone.offset = offset; + return clone; + } + + public void read(SaveState saveState) { + hexGroupSize = saveState.getInt(HEX_VIEW_GROUPSIZE_OPTION_NAME, 1); + + String charsetName = saveState.getString(CHARSET_OPTION_NAME, CharsetInfoManager.USASCII); + csi = CharsetInfoManager.getInstance().get(charsetName, StandardCharsets.US_ASCII); + + compactChars = saveState.getBoolean(COMPACTCHARS_OPTION_NAME, true); + useCharAlignment = saveState.getBoolean(USE_CHAR_ALIGNMENT_OPTION_NAME, true); + + bytesPerLine = saveState.getInt(BYTES_PER_LINE_OPTION_NAME, DEFAULT_BYTES_PER_LINE); + offset = saveState.getInt(OFFSET_NAME, 0); + } + + public void write(SaveState saveState) { + saveState.putInt(HEX_VIEW_GROUPSIZE_OPTION_NAME, hexGroupSize); + saveState.putString(CHARSET_OPTION_NAME, csi.getName()); + saveState.putBoolean(COMPACTCHARS_OPTION_NAME, compactChars); + saveState.putBoolean(USE_CHAR_ALIGNMENT_OPTION_NAME, useCharAlignment); + saveState.putInt(BYTES_PER_LINE_OPTION_NAME, bytesPerLine); + saveState.putInt(OFFSET_NAME, offset); + } + + public boolean areOptionsEqual(ByteViewerConfigOptions other) { + return bytesPerLine == other.bytesPerLine && compactChars == other.compactChars && + Objects.equals(csi, other.csi) && hexGroupSize == other.hexGroupSize && + offset == other.offset && useCharAlignment == other.useCharAlignment; + } + + public boolean areLayoutParamsChanged(ByteViewerConfigOptions other) { + return offset != other.getOffset() || hexGroupSize != other.getHexGroupSize() || + bytesPerLine != other.getBytesPerLine() || + useCharAlignment != other.isUseCharAlignment(); + } + + public boolean areDislayWidthsChanged(ByteViewerConfigOptions other) { + return getHexGroupSize() != other.getHexGroupSize() || + isCompactChars() != other.isCompactChars() || + (useCharAlignment && csi.getAlignment() != other.csi.getAlignment()); + } + + public int getBytesPerLine() { + return bytesPerLine; + } + + public void setBytesPerLine(int newBytesPerLine) { + bytesPerLine = newBytesPerLine; + offset = Math.clamp(offset, 0, bytesPerLine - 1); + hexGroupSize = Math.clamp(hexGroupSize, 1, bytesPerLine); + } + + public int getOffset() { + return offset; + } + + public int calcNormalizedOffset(int newOffset) { + if (newOffset < 0) { + newOffset = bytesPerLine - 1; + } + else if (newOffset >= bytesPerLine) { + newOffset = newOffset % bytesPerLine; + } + return newOffset; + } + + public void setOffset(int newOffset) { + offset = calcNormalizedOffset(newOffset); + } + + public int getHexGroupSize() { + return hexGroupSize; + } + + public void setHexGroupSize(int newHexGroupSize) { + hexGroupSize = newHexGroupSize; + } + + public CharsetInfo getCharsetInfo() { + return csi; + } + + public void setCharsetInfo(CharsetInfo newCSI) { + this.csi = newCSI; + } + + public void setCompactChars(boolean newCompactChars) { + compactChars = newCompactChars; + } + + public boolean isCompactChars() { + return compactChars; + } + + public boolean isUseCharAlignment() { + return useCharAlignment; + } + + public void setUseCharAlignment(boolean newUseCharAlignment) { + useCharAlignment = newUseCharAlignment; + } + +} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHighlighter.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHighlighter.java index 4c370f5891..adc9eab337 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHighlighter.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHighlighter.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,13 +18,12 @@ package ghidra.app.plugin.core.byteviewer; import java.awt.Color; import docking.widgets.fieldpanel.support.Highlight; -import generic.theme.GColor; class ByteViewerHighlighter { private static Highlight[] NO_HIGHLIGHTS = new Highlight[0]; private String highlightText; - private Color highlightColor = new GColor("color.bg.byteviewer.highlight"); + private Color highlightColor = ByteViewerComponentProvider.HIGHLIGHT_MIDDLE_MOUSE_COLOR; public Highlight[] createHighlights(String text) { @@ -41,8 +40,4 @@ class ByteViewerHighlighter { String getText() { return highlightText; } - - void setHighlightColor(Color color) { - this.highlightColor = color; - } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java index e6617e38d9..f04670db49 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,8 +25,6 @@ import javax.swing.*; import docking.widgets.fieldpanel.FieldPanel; import docking.widgets.fieldpanel.Layout; import docking.widgets.indexedscrollpane.*; -import generic.theme.GColor; -import generic.theme.Gui; import ghidra.app.plugin.core.format.DataFormatModel; /** @@ -42,7 +40,6 @@ import ghidra.app.plugin.core.format.DataFormatModel; * client's responsibility to get the header component and install it into the IndexedScrollPane. */ class ByteViewerIndexedView extends JPanel implements IndexedScrollable, IndexScrollListener { - private static final String HEADER_FONT_ID = "font.byteviewer.header"; private FieldPanel indexPanel; private List allPanels = new ArrayList<>(); private boolean processingIndexRangeChanged; @@ -53,14 +50,12 @@ class ByteViewerIndexedView extends JPanel implements IndexedScrollable, IndexSc this.indexPanel = indexPanel; allPanels.add(indexPanel); panelManager = new InteractivePanelManager(); - panelManager.setHeaderFont(Gui.getFont(HEADER_FONT_ID)); indexPanel.addIndexScrollListener(this); panelManager.addComponent(ByteViewerComponentProvider.INDEX_COLUMN_NAME, indexPanel); JComponent mainPanel = panelManager.getMainPanel(); add(mainPanel, BorderLayout.CENTER); - mainPanel.setBackground(new GColor("color.bg.byteviewer")); addMouseWheelListener(e -> { // this lets us scroll the byte viewer when the user is not over any panel, but still diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerLayoutModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerLayoutModel.java index 4173e29e82..54e7acba1f 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerLayoutModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerLayoutModel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,6 +29,7 @@ import docking.widgets.fieldpanel.listener.IndexMapper; import docking.widgets.fieldpanel.listener.LayoutModelListener; import docking.widgets.fieldpanel.support.SingleRowLayout; import ghidra.app.plugin.core.format.DataFormatModel; +import ghidra.util.datastruct.ListenerSet; /** * Implements the LayoutModel for ByteViewer Components. @@ -36,20 +37,18 @@ import ghidra.app.plugin.core.format.DataFormatModel; public class ByteViewerLayoutModel implements LayoutModel { private int width; private IndexMap indexMap; - private List listeners; + private ListenerSet listeners = + new ListenerSet<>(LayoutModelListener.class, false); private FieldFactory[] factorys; - private BigInteger numIndexes; public ByteViewerLayoutModel() { factorys = new FieldFactory[0]; - listeners = new ArrayList(1); - numIndexes = BigInteger.ZERO; } void dispose() { indexMap = null; - listeners = null; factorys = null; + listeners.clear(); } void setFactorys(FieldFactory[] fieldFactorys, DataFormatModel dataModel, int margin) { @@ -75,31 +74,19 @@ public class ByteViewerLayoutModel implements LayoutModel { return; } this.indexMap = indexMap; - if (indexMap == null) { - numIndexes = BigInteger.ZERO; - } - else { - numIndexes = indexMap.getNumIndexes(); - } indexSetChanged(); } public void indexSetChanged() { - for (LayoutModelListener listener : listeners) { - listener.modelSizeChanged(IndexMapper.IDENTITY_MAPPER); - } + listeners.invoke().modelSizeChanged(IndexMapper.IDENTITY_MAPPER); } public void layoutChanged() { - for (LayoutModelListener listener : listeners) { - listener.dataChanged(BigInteger.ZERO, numIndexes); - } + listeners.invoke().dataChanged(BigInteger.ZERO, getNumIndexes()); } public void dataChanged(BigInteger startIndex, BigInteger endIndex) { - for (LayoutModelListener listener : listeners) { - listener.dataChanged(startIndex, endIndex); - } + listeners.invoke().dataChanged(startIndex, endIndex); } @Override @@ -117,12 +104,12 @@ public class ByteViewerLayoutModel implements LayoutModel { */ @Override public BigInteger getNumIndexes() { - return numIndexes; + return indexMap != null ? indexMap.getNumIndexes() : BigInteger.ZERO; } @Override public Layout getLayout(BigInteger index) { - if (index.compareTo(numIndexes) >= 0) { + if (index.compareTo(BigInteger.ZERO) < 0 || index.compareTo(getNumIndexes()) >= 0) { return null; } List fields = new ArrayList(factorys.length); @@ -158,17 +145,10 @@ public class ByteViewerLayoutModel implements LayoutModel { listeners.add(listener); } - /** - * @see docking.widgets.fieldpanel.LayoutModel#getIndexAfter(int) - */ - public int getIndexAfter(int index) { - return index + 1; - } - @Override public BigInteger getIndexAfter(BigInteger index) { BigInteger nextIndex = index.add(BigInteger.ONE); - if (nextIndex.compareTo(numIndexes) >= 0) { + if (nextIndex.compareTo(getNumIndexes()) >= 0) { return null; } return nextIndex; @@ -176,13 +156,15 @@ public class ByteViewerLayoutModel implements LayoutModel { @Override public BigInteger getIndexBefore(BigInteger index) { + BigInteger numIndexes = getNumIndexes(); + if (numIndexes.compareTo(BigInteger.ZERO) <= 0 || index.compareTo(BigInteger.ZERO) <= 0) { + return null; + } + if (index.compareTo(numIndexes) > 0) { return numIndexes.subtract(BigInteger.ONE); } BigInteger previousIndex = index.subtract(BigInteger.ONE); - if (previousIndex.compareTo(BigInteger.ZERO) < 0) { - return null; - } return previousIndex; } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerOptionsDialog.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerOptionsDialog.java index de061e0fbd..d5d8a1a6e1 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerOptionsDialog.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerOptionsDialog.java @@ -15,47 +15,55 @@ */ package ghidra.app.plugin.core.byteviewer; -import java.awt.Component; -import java.awt.GridLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.awt.*; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; import java.math.BigInteger; import java.util.*; -import java.util.Map.Entry; +import java.util.stream.Collectors; import javax.swing.*; import javax.swing.border.Border; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import org.apache.commons.lang3.StringUtils; import docking.DialogComponentProvider; +import docking.widgets.button.BrowseButton; import docking.widgets.checkbox.GCheckBox; +import docking.widgets.fieldpanel.support.ViewerPosition; import docking.widgets.label.GLabel; +import docking.widgets.spinner.IntegerSpinner; import generic.theme.GThemeDefaults.Colors; import generic.theme.GThemeDefaults.Colors.Messages; import ghidra.app.plugin.core.format.DataFormatModel; -import ghidra.app.util.AddressInput; -import ghidra.app.util.bean.FixedBitSizeValueField; -import ghidra.program.model.address.Address; -import ghidra.program.model.listing.Program; import ghidra.util.HelpLocation; +import ghidra.util.charset.CharsetInfo; +import ghidra.util.charset.picker.CharsetPickerDialog; import ghidra.util.layout.PairLayout; import ghidra.util.layout.VerticalLayout; -public class ByteViewerOptionsDialog extends DialogComponentProvider - implements ChangeListener, ActionListener { +public class ByteViewerOptionsDialog extends DialogComponentProvider { - private AddressInput addressInputField; - private FixedBitSizeValueField bytesPerLineField; - private FixedBitSizeValueField groupSizeField; + private JTextField charsetField; + private BrowseButton charsetPickerButton; private ByteViewerComponentProvider provider; + private ByteViewerConfigOptions configOptions; private Map checkboxMap = new HashMap<>(); + private MySpinnerNumberModel bytesPerLineSpinnerModel; + private MySpinnerNumberModel offsetSpinnerModel; + private MySpinnerNumberModel hexGroupSizeSpinnerModel; + private IntegerSpinner bytesPerLineSpinner; + private IntegerSpinner offsetSpinner; + private IntegerSpinner hexGroupSizeSpinner; + private LinkedHashMap models = new LinkedHashMap<>(); public ByteViewerOptionsDialog(ByteViewerComponentProvider provider) { super("Byte Viewer Options"); this.provider = provider; + this.configOptions = provider.getConfigOptions().clone(); + + for (String modelName : provider.getDataFormatNames()) { + models.put(modelName, provider.getDataFormatModel(modelName)); + } + addWorkPanel(buildPanel()); addOKButton(); addCancelButton(); @@ -65,258 +73,336 @@ public class ByteViewerOptionsDialog extends DialogComponentProvider setRememberSize(false); } + private void disposeModels() { + for (DataFormatModel model : models.values()) { + model.dispose(); + } + models.clear(); + } + private JComponent buildPanel() { JPanel mainPanel = new JPanel(new VerticalLayout(10)); mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); mainPanel.add(buildSettingsPanel()); - mainPanel.add(buildViewOptionsPanel()); - setOkEnabled(hasValidFieldValues()); + mainPanel.add(buildModelPickerPanel()); + updateButtonEnablement(); return mainPanel; } + private void setTooltip(IntegerSpinner spinner, String text) { + spinner.getTextField().getComponent().setToolTipText(text); + spinner.getSpinner().setToolTipText(text); + } + + private void updateModelStatus(DataFormatModel model, String errorMsg) { + JCheckBox cb = checkboxMap.get(model.getName()); + if (cb != null) { + cb.setForeground(errorMsg == null ? Colors.FOREGROUND : Messages.ERROR); + cb.setToolTipText(errorMsg); + } + } + + private boolean isModelEnabled(DataFormatModel model) { + JCheckBox cb = checkboxMap.get(model.getName()); + return cb != null && cb.isSelected(); + } + + private void updateButtonEnablement() { + int enabledModelCount = 0; + String firstErrorMsg = null; + for (DataFormatModel model : models.values()) { + String errorMsg = model.validateByteViewerConfigOptions(configOptions); + if (errorMsg == null && + configOptions.getBytesPerLine() % model.getUnitByteSize() != 0) { + errorMsg = "%s (%d bytes) is not a multiple of %d".formatted(model.getName(), + model.getUnitByteSize(), configOptions.getBytesPerLine()); + } + updateModelStatus(model, errorMsg); + if (isModelEnabled(model)) { + enabledModelCount++; + if (errorMsg != null) { + firstErrorMsg = firstErrorMsg == null ? errorMsg : firstErrorMsg; + } + } + } + if (enabledModelCount == 0) { + firstErrorMsg = "You must have at least one view selected"; + } + + setStatusText(firstErrorMsg); + setOkEnabled(firstErrorMsg == null); + } + private Component buildSettingsPanel() { JPanel panel = new JPanel(new PairLayout(5, 5)); panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - panel.add(new GLabel("Alignment Address:")); - buildAddressField(panel); + bytesPerLineSpinnerModel = + new MySpinnerNumberModel(configOptions.getBytesPerLine(), 1, 256, 1); + offsetSpinnerModel = new MySpinnerNumberModel(configOptions.getOffset(), 0, + configOptions.getBytesPerLine() - 1, 1) { + // wrap around the top and bottom of the valid range + @Override + public Object getNextValue() { + Long val = (Long) getNumber(); + Long maximum = (Long) getMaximum(); + if (maximum != null && maximum.compareTo(val) <= 0) { + return getMinimum(); + } + return super.getNextValue(); + } + + @Override + public Object getPreviousValue() { + Long val = (Long) getNumber(); + Long min = (Long) getMinimum(); + if (min != null && min.compareTo(val) >= 0) { + return getMaximum(); + } + return super.getPreviousValue(); + } + }; + hexGroupSizeSpinnerModel = new MySpinnerNumberModel(configOptions.getHexGroupSize(), 1, + configOptions.getBytesPerLine(), 1); + + bytesPerLineSpinner = new MyIntegerSpinner(bytesPerLineSpinnerModel, 3); + bytesPerLineSpinner.getTextField().setShowNumberMode(false); + bytesPerLineSpinner.getTextField().setAccessibleName("Bytes Per Line"); + setTooltip(bytesPerLineSpinner, "Number of bytes to display in each row of the viewer."); + + offsetSpinner = new MyIntegerSpinner(offsetSpinnerModel, 3); + offsetSpinner.getTextField().setShowNumberMode(false); + offsetSpinner.getTextField().setAccessibleName("Offset"); + setTooltip(offsetSpinner, "Adjusts the starting byte of the row left or right.\n" + + "Ranges from 0 to bytes per line (exclusive)."); + + hexGroupSizeSpinner = new MyIntegerSpinner(hexGroupSizeSpinnerModel, 3); + hexGroupSizeSpinner.getTextField().setShowNumberMode(false); + hexGroupSizeSpinner.getTextField().setAccessibleName("Hex Group Size"); + setTooltip(hexGroupSizeSpinner, + "How many bytes will be grouped together in the hex view.\n" + + "Ranges from 1 to bytes per line (inclusive)."); + + bytesPerLineSpinnerModel.addChangeListener(e -> { + Long bpl = (Long) bytesPerLineSpinnerModel.getNumber(); + if (bpl != null) { + setBytesPerLine(bpl.intValue()); + } + }); + + offsetSpinnerModel.addChangeListener(e -> { + Long val = (Long) offsetSpinnerModel.getNumber(); + if (val != null) { + setOffset(val.intValue()); + } + }); + + hexGroupSizeSpinnerModel.addChangeListener(e -> { + Long val = (Long) hexGroupSizeSpinnerModel.getNumber(); + if (val != null) { + setHexGroupSize(val.intValue()); + } + }); panel.add(new GLabel("Bytes Per Line:")); - bytesPerLineField = new FixedBitSizeValueField(8, false, true); - bytesPerLineField.setFormat(10, false); - bytesPerLineField.setMinMax(BigInteger.valueOf(1), BigInteger.valueOf(256)); - bytesPerLineField.setValue(BigInteger.valueOf(provider.getBytesPerLine())); - panel.add(bytesPerLineField); - bytesPerLineField.addChangeListener(this); - bytesPerLineField.getAccessibleContext().setAccessibleName("Bytes Per Line"); + panel.add(bytesPerLineSpinner.getSpinner()); - panel.add(new GLabel("Group size (Hex View Only):")); - groupSizeField = new FixedBitSizeValueField(8, false, true); - groupSizeField.setFormat(10, false); - groupSizeField.setMinMax(BigInteger.valueOf(1), BigInteger.valueOf(256)); - groupSizeField.setValue(BigInteger.valueOf(provider.getGroupSize())); - panel.add(groupSizeField); - groupSizeField.addChangeListener(this); - groupSizeField.getAccessibleContext().setAccessibleName("Group Size"); + panel.add(new GLabel("Alignment Offset:")); + panel.add(offsetSpinner.getSpinner()); + + panel.add(new GLabel("Hex Group Size:")); + panel.add(hexGroupSizeSpinner.getSpinner()); + + panel.add(new GLabel("Charset:")); + charsetField = new JTextField(); + charsetField.setEditable(false); + charsetField.setText(configOptions.getCharsetInfo().getName()); + charsetField.getAccessibleContext().setAccessibleName("Character Set Name"); + + charsetPickerButton = new BrowseButton(); + charsetPickerButton.addActionListener(e -> pickCharset()); + charsetPickerButton.getAccessibleContext().setAccessibleName("Character Set Picker"); + JPanel charsetPanel = new JPanel(new BorderLayout()); + charsetPanel.add(charsetField, BorderLayout.CENTER); + charsetPanel.add(charsetPickerButton, BorderLayout.EAST); + panel.add(charsetPanel); + + panel.add(new GLabel("Compact Char Width:")); + GCheckBox compactChars = new GCheckBox(); + compactChars.setSelected(configOptions.isCompactChars()); + compactChars.addChangeListener(e -> { + configOptions.setCompactChars(compactChars.isSelected()); + // doesn't affect ok enablement, no need to call updateButtonEnablement() + }); + compactChars.getAccessibleContext().setAccessibleName("Compact Characters"); + compactChars.setToolTipText( + "Display characters tightly packed together or more widely spaced apart"); + panel.add(compactChars); + + panel.add(new GLabel("Use Char Alignment:")); + GCheckBox useCharAlignment = new GCheckBox(); + useCharAlignment.setSelected(configOptions.isUseCharAlignment()); + useCharAlignment.addChangeListener(e -> { + configOptions.setUseCharAlignment(useCharAlignment.isSelected()); + // doesn't affect ok enablement, no need to call updateButtonEnablement() + }); + useCharAlignment.getAccessibleContext().setAccessibleName("Character Alignment"); + useCharAlignment + .setToolTipText("Align start-of-character location with charset's byte width.\n" + + "Only some charsets (like UTF-16/UTF-32) are marked as alignable."); + panel.add(useCharAlignment); return panel; } - private void buildAddressField(JPanel parentPanel) { - if (!(provider instanceof ProgramByteViewerComponentProvider programProvider)) { - buildSimpleAddressInput(parentPanel); - return; + void setBytesPerLine(int bpl) { + configOptions.setBytesPerLine(bpl); + if (configOptions.getOffset() != offsetSpinnerModel.getNumber().intValue()) { + offsetSpinnerModel.setValue(Long.valueOf(configOptions.getOffset())); } - - Program program = programProvider.getProgram(); - if (program == null) { - buildSimpleAddressInput(parentPanel); - return; + if (configOptions.getHexGroupSize() != hexGroupSizeSpinnerModel.getIntValue()) { + hexGroupSizeSpinnerModel.setValue(Long.valueOf(configOptions.getHexGroupSize())); } + offsetSpinnerModel.setMaximum(Long.valueOf(bpl - 1)); + hexGroupSizeSpinnerModel.setMaximum(Long.valueOf(bpl)); - Address alignment = getAlignmentAddress(); - if (alignment == null) { - buildSimpleAddressInput(parentPanel); - return; - } - - addressInputField = new AddressInput(program, a -> update()); - addressInputField.setAccessibleName("Alignment Address"); - addressInputField.setAddressSpaceFilter(s -> s == alignment.getAddressSpace()); - addressInputField.setAddress(alignment); - parentPanel.add(addressInputField); + updateButtonEnablement(); } - private void buildSimpleAddressInput(JPanel parentPanel) { - addressInputField = new AddressInput(); - addressInputField.setAccessibleName("Alignment Address"); - addressInputField.setEnabled(false); - parentPanel.add(addressInputField); + public void setOffset(int val) { + configOptions.setOffset(val); + updateButtonEnablement(); } - private Component buildViewOptionsPanel() { + void setHexGroupSize(int val) { + configOptions.setHexGroupSize(val); + updateButtonEnablement(); + } + + private void pickCharset() { + CharsetInfo newCSI = CharsetPickerDialog.pickCharset(configOptions.getCharsetInfo()); + if (newCSI != null) { + setCharsetInfo(newCSI); + } + } + + private void setCharsetInfo(CharsetInfo newCSI) { + configOptions.setCharsetInfo(newCSI); + charsetField.setText(newCSI.getName()); + } + + private JPanel buildModelPickerPanel() { JPanel panel = new JPanel(new GridLayout(0, 2, 40, 0)); Border outer = BorderFactory.createTitledBorder("Views"); Border inner = BorderFactory.createEmptyBorder(5, 15, 5, 15); panel.setBorder(BorderFactory.createCompoundBorder(outer, inner)); Set currentViews = provider.getCurrentViews(); - List dataModelNames = provider.getDataFormatNames(); - for (String formatName : dataModelNames) { - GCheckBox checkBox = new GCheckBox(formatName); - checkBox.addActionListener(this); - checkboxMap.put(formatName, checkBox); - if (currentViews.contains(formatName)) { - checkBox.setSelected(true); + for (DataFormatModel model : models.values()) { + String modelName = model.getName(); + GCheckBox cb = new GCheckBox(modelName); + cb.addChangeListener(e -> updateButtonEnablement()); + checkboxMap.put(modelName, cb); + if (currentViews.contains(modelName)) { + cb.setSelected(true); } - panel.add(checkBox); + panel.add(cb); } return panel; } - private Address getAlignmentAddress() { - int bytesPerLine = provider.getBytesPerLine(); - int offset = provider.getOffset(); - - Program program = ((ProgramByteViewerComponentProvider) provider).getProgram(); - Address minAddr = program.getMinAddress(); - if (minAddr == null) { - return null; - } - - long addressOffset = minAddr.getOffset() + offset; - int alignment = (int) (addressOffset % bytesPerLine); - return (alignment == 0) ? minAddr : minAddr.add(bytesPerLine - alignment); + void setModelSelected(String modelName, boolean selected) { + JCheckBox cb = checkboxMap.get(modelName); + cb.setSelected(selected); } @Override protected void okCallback() { - int bytesPerLine = bytesPerLineField.getValue().intValue(); - int addrOffset = 0; - Address alignmentAddress = addressInputField.getAddress(); - if (alignmentAddress != null) { - addrOffset = (int) (alignmentAddress.getOffset() % bytesPerLine); - } - - // We want the alignment address to begin a column, so subtract addrOffset from bytesPerLine - int offset = addrOffset == 0 ? 0 : bytesPerLine - addrOffset; - int groupSize = groupSizeField.getValue().intValue(); - - removeDeletedViews(); - - // Setting these properties individually is problematic since it can temporarily put the - // system into a bad state. As a hack, set the bytes per line to 256 since that can support - // all allowed group sizes. Then set the group first since there will be a divide by zero - // exception if the group size is ever bigger than the bytes per line. Finally, after all - // setting have been updated, add in the newly added views. - provider.setBytesPerLine(256); - provider.setGroupSize(groupSize); - provider.setBytesPerLine(bytesPerLine); - provider.setBlockOffset(offset); - - addNewViews(); - + ViewerPosition vp = provider.getByteViewerPanel().getViewerPosition(); + provider.updateConfigOptions(configOptions, getSelectedViewNames()); + provider.getByteViewerPanel().setViewerPosition(vp); + disposeModels(); close(); } - private void removeDeletedViews() { - Set currentViews = provider.getCurrentViews(); - - for (String viewName : currentViews) { - JCheckBox checkBox = checkboxMap.get(viewName); - if (!checkBox.isSelected()) { - provider.removeView(viewName, true); - } - } - } - - private void addNewViews() { - Set currentViews = provider.getCurrentViews(); - - // now add any views that have been selected - for (String viewName : checkboxMap.keySet()) { - JCheckBox checkBox = checkboxMap.get(viewName); - if (!currentViews.contains(viewName) && checkBox.isSelected()) { - provider.addView(viewName); - } - } - - } - @Override - public void actionPerformed(ActionEvent e) { - update(); + protected void cancelCallback() { + disposeModels(); + super.cancelCallback(); } - @Override - public void stateChanged(ChangeEvent e) { - update(); + private Set getSelectedViewNames() { + return checkboxMap.entrySet() + .stream() + .filter(entry -> entry.getValue().isSelected()) + .map(entry -> entry.getKey()) + .collect(Collectors.toSet()); } - private void update() { - setOkEnabled(hasValidFieldValues()); - } + private static class MySpinnerNumberModel extends SpinnerNumberModel { - private boolean hasValidFieldValues() { - if (!validateAddress()) { - return false; + public MySpinnerNumberModel(int value, int minimum, int maximum, int stepSize) { + super(Long.valueOf(value), Long.valueOf(minimum), Long.valueOf(maximum), + Long.valueOf(stepSize)); } - BigInteger bytesPerLine = bytesPerLineField.getValue(); - if (bytesPerLine == null) { - setStatusText("Enter a value for Bytes Per Line"); - return false; + public int getIntValue() { + return ((Long) getValue()).intValue(); } - BigInteger groupSize = groupSizeField.getValue(); - if (groupSize == null) { - setStatusText("Enter a group size"); - return false; - } - if (bytesPerLine.intValue() % groupSize.intValue() != 0) { - setStatusText("The bytes per line must be a multiple of the group size."); - return false; + public boolean isValid(Object value) { + if (value == null || !(value instanceof Long val)) { + return false; + } + Long minimum = (Long) getMinimum(); + Long maximum = (Long) getMaximum(); + + if (minimum.compareTo(val) > 0 || maximum.compareTo(val) < 0) { + return false; + } + + return true; } - if (checkForUnsupportedModels(bytesPerLine.intValue())) { - setStatusText("Not all selected views support the current bytes per line value."); - return false; - } - - if (!atLeastOneViewOn()) { - setStatusText("You must have at least one view selected"); - return false; - } - - setStatusText(""); - return true; - } - - private boolean validateAddress() { - if (!addressInputField.isEnabled()) { - return true; // nothing to validate - } - - String addrText = addressInputField.getText(); - if (StringUtils.isBlank(addrText)) { - setStatusText("Enter an alignment address"); - return false; - } - - Address alignmentAddress = addressInputField.getAddress(); - if (alignmentAddress == null) { - setStatusText("Invalid alignment address:" + addrText); - return false; - } - return true; - } - - private boolean atLeastOneViewOn() { - Set> entrySet = checkboxMap.entrySet(); - for (Entry entry : entrySet) { - JCheckBox checkBox = entry.getValue(); - if (checkBox.isSelected()) { - return true; + @Override + public void setValue(Object value) { + if (isValid(value)) { + super.setValue(value); } } - return false; } - private boolean checkForUnsupportedModels(int bytesPerLine) { - boolean isBad = false; - Set> entrySet = checkboxMap.entrySet(); - for (Entry entry : entrySet) { - JCheckBox checkBox = entry.getValue(); - DataFormatModel model = provider.getDataFormatModel(entry.getKey()); - if (model.validateBytesPerLine(bytesPerLine)) { - checkBox.setForeground(Colors.FOREGROUND); - } - else { - checkBox.setForeground(Messages.ERROR); - isBad |= checkBox.isSelected(); - } + private static class MyIntegerSpinner extends IntegerSpinner { + // change color of text field to red if value that the user manually entered conflicts with + // spinner model, and when the focus leaves the text field, validate the text and replace + // it with the current model value if invalid + MyIntegerSpinner(MySpinnerNumberModel spinnerModel, int columns) { + super(spinnerModel, columns); + + integerTextField.addChangeListener(e -> { + BigInteger valObj = integerTextField.getValue(); + Long value = valObj != null ? valObj.longValue() : null; + integerTextField.getComponent() + .setForeground( + spinnerModel.isValid(value) ? Colors.FOREGROUND : Messages.ERROR); + }); + integerTextField.getComponent().addFocusListener(new FocusListener() { + @Override + public void focusLost(FocusEvent e) { + BigInteger valObj = integerTextField.getValue(); + Long value = valObj != null ? valObj.longValue() : null; + if (!spinnerModel.isValid(value)) { + integerTextField.setValue(spinnerModel.getIntValue()); + } + } + + @Override + public void focusGained(FocusEvent e) { + // nothing + } + }); } - return isBad; } + } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java index 56f1d59270..422df01944 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java @@ -21,7 +21,9 @@ import java.util.ArrayList; import java.util.List; import javax.swing.*; +import javax.swing.border.BevelBorder; +import docking.widgets.EventTrigger; import docking.widgets.fieldpanel.*; import docking.widgets.fieldpanel.field.EmptyTextField; import docking.widgets.fieldpanel.field.Field; @@ -36,7 +38,7 @@ import ghidra.app.util.viewer.listingpanel.AddressSetDisplayListener; import ghidra.program.model.address.AddressSet; import ghidra.program.model.address.AddressSetView; import ghidra.util.*; -import ghidra.util.exception.InvalidInputException; +import ghidra.util.datastruct.ListenerSet; import ghidra.util.layout.PairLayout; import help.Help; import help.HelpService; @@ -63,20 +65,19 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen private IndexMap indexMap; // maps indexes to the correct block and offset private int blockOffset; private ByteViewerComponent currentView; - - private Color highlightColor; private int highlightButton; - private List layoutListeners = new ArrayList<>(1); + private ListenerSet layoutListeners = + new ListenerSet<>(LayoutModelListener.class, false); private boolean addingView; // don't respond to cursor location changes while this flag is true - private final ByteViewerComponentProvider provider; + private List displayListeners = new ArrayList<>(); private ByteViewerIndexedView indexedView; + private boolean editMode; protected ByteViewerPanel(ByteViewerComponentProvider provider) { - super(); this.provider = provider; - bytesPerLine = ByteViewerComponentProvider.DEFAULT_BYTES_PER_LINE; + bytesPerLine = provider.getConfigOptions().getBytesPerLine(); viewList = new ArrayList<>(); indexMap = new IndexMap(); create(); @@ -106,30 +107,18 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen return new Dimension(width, height); } - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); + void updateColors() { + for (ByteViewerComponent comp : viewList) { + comp.updateColors(); + } + } + + int getHighlightButton() { + return highlightButton; } void setHighlightButton(int highlightButton) { this.highlightButton = highlightButton; - for (ByteViewerComponent comp : viewList) { - comp.setHighlightButton(highlightButton); - } - } - - void setMouseButtonHighlightColor(Color color) { - this.highlightColor = color; - for (ByteViewerComponent comp : viewList) { - comp.setMouseButtonHighlightColor(color); - } - } - - void setSeparatorColor(Color c) { - indexFactory.setMissingValueColor(c); - for (ByteViewerComponent comp : viewList) { - comp.setSeparatorColor(c); - } } /** @@ -151,12 +140,17 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen ByteBlock lastBlock = blocks[blocks.length - 1]; endField.setText(lastBlock .getLocationRepresentation(lastBlock.getLength().subtract(BigInteger.ONE))); + offsetField.setText(Integer.toString(blockOffset)); clearSelection(); } } if (indexMap == null) { indexMap = new IndexMap(); + startField.setText("00000000"); + endField.setText("00000000"); + offsetField.setText("00000000"); + insertionField.setText("00000000"); } indexFactory.setIndexMap(indexMap); indexFactory.setSize(getIndexSizeInChars()); @@ -198,12 +192,6 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen } } - public void setViewerBackgroundColorModel(BackgroundColorModel colorModel) { - for (ByteViewerComponent c : viewList) { - c.setBackgroundColorModel(colorModel); - } - } - /** * Get the current highlight. * @@ -220,7 +208,6 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen * Called by the plugin in response to an event. */ void setCursorLocation(ByteBlock block, BigInteger index, int column) { - int modelIndex = -1; for (ByteViewerComponent c : viewList) { modelIndex = c.setViewerCursorLocation(block, index, column); @@ -228,6 +215,13 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen if (modelIndex >= 0) { insertionField.setText(block.getLocationRepresentation(index)); } + updateIndexColumnCurrentLine(); + } + + void updateIndexColumnCurrentLine() { + // this needs to be called by each ByteViewerComponent when the line index for their + // cursor changes so that the address column can be updated + indexPanel.repaint(); } /** @@ -257,9 +251,17 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen return currentView; } + public ByteViewerComponent getComponentByName(String name) { + for (ByteViewerComponent bvc : viewList) { + if (name.equals(bvc.getDataModel().getName())) { + return bvc; + } + } + return null; + } + protected ByteViewerComponent newByteViewerComponent(DataFormatModel model) { - return new ByteViewerComponent(this, new ByteViewerLayoutModel(), model, bytesPerLine, - fontMetrics); + return new ByteViewerComponent(this, new ByteViewerLayoutModel(), model, bytesPerLine); } /** @@ -281,11 +283,7 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen ViewerPosition vp = getViewerPosition(); ByteViewerComponent c = newByteViewerComponent(model); - - c.setEditMode(editMode); c.setIndexMap(indexMap); - c.setMouseButtonHighlightColor(highlightColor); - c.setHighlightButton(highlightButton); viewList.add(c); c.setSize(c.getPreferredSize()); indexedView.addView(viewName, c); @@ -318,6 +316,7 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen } addingView = false; } + c.updateColors(); validate(); repaint(); return c; @@ -329,12 +328,9 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen indexedView.removeView(comp); if (currentView == comp) { - currentView = null; + currentView = !viewList.isEmpty() ? viewList.get(0) : null; } - if (viewList.size() > 0) { - currentView = viewList.get(0); - } comp.dispose(); validate(); repaint(); @@ -342,19 +338,16 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen void setCurrentView(ByteViewerComponent c) { currentView = c; + updateColors(); } void setEditMode(boolean editMode) { - for (ByteViewerComponent c : viewList) { - c.setEditMode(editMode); - } + this.editMode = editMode; + updateColors(); } boolean getEditMode() { - if (currentView == null) { - return false; - } - return currentView.getEditMode(); + return editMode; } /** @@ -366,25 +359,23 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen } } - int getNumberOfViews() { - return viewList.size(); - } + void updateLayoutConfigOptions(ByteViewerConfigOptions options) { + boolean bplChanged = bytesPerLine != options.getBytesPerLine(); + boolean offsetChanged = blockOffset != options.getOffset(); + if (bplChanged || offsetChanged) { + bytesPerLine = options.getBytesPerLine(); + blockOffset = options.getOffset(); - void setOffset(int offset) { - if (blockOffset != offset) { - blockOffset = offset; updateIndexMap(); - offsetField.setText(Integer.toString(offset)); + offsetField.setText(Integer.toString(blockOffset)); + } + if (bplChanged) { + // reset view column widths to preferred width for new bytesPerline + resetColumnsToDefaultWidths(); } } - void setBytesPerLine(int bytesPerLine) { - - if (this.bytesPerLine != bytesPerLine) { - this.bytesPerLine = bytesPerLine; - updateIndexMap(); - } - // reset view column widths to preferred width for new bytesPerline + void resetColumnsToDefaultWidths() { indexedView.resetViewWidthToDefaults(); // force everything to get validated, or else the @@ -394,47 +385,6 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen repaint(); } - /** - * Check that each model for the views can support the given bytes per line value. - * @param numBytesPerLine the bytes per line value to see if supported - * - * @throws InvalidInputException if a model cannot support the bytesPerLine value - */ - void checkBytesPerLine(int numBytesPerLine) throws InvalidInputException { - for (ByteViewerComponent c : viewList) { - DataFormatModel model = c.getDataModel(); - int groupSize = model.getGroupSize(); - if (groupSize > 0) { - if (numBytesPerLine % groupSize != 0) { - throw new InvalidInputException( - "Bytes Per Line not divisible by Group Size[" + groupSize + "]."); - } - } - } - } - - /** - * Set the group size on the current view. - * - * @param groupSize new group size - */ - void setCurrentGroupSize(int groupSize) { - if (currentView == null) { - return; - } - ByteBlockInfo info = currentView.getViewerCursorLocation(); - currentView.setGroupSize(groupSize); - if (info != null) { - setCursorLocation(info.getBlock(), info.getOffset(), info.getColumn()); - } - // force everything to get validated, or else the - // header columns do not get repainted properly... - - invalidate(); - validate(); - repaint(); - } - /** * Set the insertion field and tell other views to change location; called when the * ByteViewerComponent receives a notification that the cursor location has changed. @@ -448,7 +398,6 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen */ void setInsertionField(ByteViewerComponent source, ByteBlock block, BigInteger offset, BigInteger modelIndex, int column, boolean isAltDown) { - provider.updateLocation(block, offset, column, isAltDown); if (addingView) { @@ -468,6 +417,16 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen } c.setViewerCursorLocation(block, offset, column); } + updateIndexColumnCurrentLine(); + } + + void setCurrentNonMappedIndex(BigInteger index, ByteViewerComponent source) { + // used to update all viewer columns to a line index that isn't mapped to a byte offset + for (ByteViewerComponent c : viewList) { + if (c != source) { + c.setCursorPosition(index, 0, 0, 0, EventTrigger.INTERNAL_ONLY); + } + } } /** @@ -490,10 +449,7 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen void updateLiveSelection(ByteViewerComponent source, ByteBlockSelection selection) { provider.updateLiveSelection(selection); - } - - FontMetrics getCurrentFontMetrics() { - return fontMetrics; + updateIndexColumnCurrentLine(); } List getViewNamesInDisplayOrder() { @@ -528,15 +484,8 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen indexPanel.setViewerPosition(vpos.getIndex(), vpos.getXOffset(), vpos.getYOffset()); } - void restoreConfigState(int newBytesPerLine, int offset) { - if (blockOffset != offset) { - blockOffset = offset; - offsetField.setText(Integer.toString(offset)); - if (this.bytesPerLine == newBytesPerLine) { - updateIndexMap(); - } - } - setBytesPerLine(newBytesPerLine); + void restoreConfigState(ByteViewerConfigOptions options) { + updateLayoutConfigOptions(options); } void programWasRestored() { @@ -544,34 +493,29 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen refreshView(); } - void setFontMetrics(FontMetrics fm) { - this.fontMetrics = fm; - for (ByteViewerComponent c : viewList) { - c.setFontMetrics(fm); - } - indexFactory = new IndexFieldFactory(fm); - indexFactory.setSize(getIndexSizeInChars()); - updateIndexMap(); - indexPanel.modelSizeChanged(IndexMapper.IDENTITY_MAPPER); - } - - protected FontMetrics getFontMetrics() { - return fontMetrics; - } - protected int getBytesPerLine() { return bytesPerLine; } + void dispose() { + for (ByteViewerComponent comp : viewList) { + comp.dispose(); + } + viewList.clear(); + indexMap = new IndexMap(); + blockSet = null; + layoutListeners.clear(); + } + /** * Create the components for this top level panel. */ private void create() { setLayout(new BorderLayout(10, 0)); - - fontMetrics = getFontMetrics(Gui.getFont(ByteViewerComponentProvider.DEFAULT_FONT_ID)); - fontHeight = fontMetrics.getHeight(); + setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); + + setFont(ByteViewerComponentProvider.DEFAULT_FONT); // side-effect sets fontMetrics // for the index/address column indexFactory = new IndexFieldFactory(fontMetrics); @@ -581,16 +525,22 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen indexPanel.setCursorOn(false); indexPanel.setFocusable(false); indexPanel.addLayoutListener(this); + indexPanel.setBackgroundColor(ByteViewerComponentProvider.BG_COLOR); + indexPanel.setBackgroundColorModel( + new ByteViewerBGColorModel(() -> getCurrentComponent().getCursorLocation().getIndex())); indexedView = new ByteViewerIndexedView(indexPanel); IndexedScrollPane indexedScrollPane = new IndexedScrollPane(indexedView); indexedScrollPane.setWheelScrollingEnabled(false); indexedScrollPane.setColumnHeaderComp(indexedView.getColumnHeader()); + indexedScrollPane.setBackground(ByteViewerComponentProvider.BG_COLOR); statusPanel = createStatusPanel(); add(indexedScrollPane, BorderLayout.CENTER); add(statusPanel, BorderLayout.SOUTH); + Gui.registerFont(this, ByteViewerComponentProvider.DEFAULT_FONT_ID); + HelpService help = Help.getHelpService(); help.registerHelp(this, new HelpLocation("ByteViewerPlugin", "ByteViewerPlugin")); } @@ -713,10 +663,14 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen @Override public Layout getLayout(BigInteger index) { + // creates the field layout for the specified index line in the Address column + if (index.compareTo(getNumIndexes()) >= 0) { + return null; + } + Field field = indexFactory.getField(index); if (field == null) { - int height = indexFactory.getMetrics().getMaxAscent() + - indexFactory.getMetrics().getMaxDescent(); + int height = fontMetrics.getMaxAscent() + fontMetrics.getMaxDescent(); field = new EmptyTextField(height, indexFactory.getStartX(), 0, indexFactory.getWidth()); } @@ -734,9 +688,7 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen } void indexSetChanged() { - for (LayoutModelListener listener : layoutListeners) { - listener.modelSizeChanged(IndexMapper.IDENTITY_MAPPER); - } + layoutListeners.invoke().modelSizeChanged(IndexMapper.IDENTITY_MAPPER); } private int getIndexSizeInChars() { @@ -794,7 +746,7 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen protected AddressSetView computeVisibleAddresses(List layouts) { // Kind of gross, but current component will do ByteViewerComponent component = getCurrentComponent(); - if (component == null || blockSet == null) { + if (component == null || blockSet == null || layouts.isEmpty()) { return new AddressSet(); } @@ -835,4 +787,19 @@ public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListen public void setViewWidth(String viewName, int width) { indexedView.setColumnWidth(viewName, width); } + + private void updateFontDependantInfo() { + fontMetrics = getFontMetrics(getFont()); + fontHeight = fontMetrics.getHeight(); + if (indexFactory != null) { + indexFactory.setFontMetrics(fontMetrics); + } + } + + @Override + public void setFont(Font font) { + super.setFont(font); + updateFontDependantInfo(); + } + } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/EmptyByteBlockSet.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/EmptyByteBlockSet.java index 376d4b22ba..1ef7868240 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/EmptyByteBlockSet.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/EmptyByteBlockSet.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,8 +24,14 @@ import ghidra.program.model.address.AddressSet; public class EmptyByteBlockSet implements ByteBlockSet { + @Override + public boolean isValid() { + return false; + } + @Override public void dispose() { + // nothing } @Override @@ -52,6 +58,7 @@ public class EmptyByteBlockSet implements ByteBlockSet { @Override public void notifyByteEditing(ByteBlock block, BigInteger index, byte[] oldValue, byte[] newValue) { + // nothing } @Override diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FieldFactory.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FieldFactory.java index f65b4a341f..d5edf925a9 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FieldFactory.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FieldFactory.java @@ -15,7 +15,6 @@ */ package ghidra.app.plugin.core.byteviewer; -import java.awt.Color; import java.awt.FontMetrics; import java.math.BigInteger; @@ -26,36 +25,41 @@ import ghidra.app.plugin.core.format.*; import ghidra.program.model.address.AddressOutOfBoundsException; /** - * Implementation of Field for showing dated formatted according to a - * DataFormatModel. + * Implementation of Field for showing data formatted according to a + * {@link DataFormatModel}. + *

    + * An instance of this class will be created for each independent position in a byteviewer + * row (typically 16). */ class FieldFactory { private IndexMap indexMap; // maps index to a block and offset into the block private ByteBlockSet blockSet; private DataFormatModel model; - private int charWidth; // width in pixels + private final int charWidth; // width in pixels private int fieldOffset; private FontMetrics fm; private int width; // field width private String noValueStr; private String readErrorStr; // string to use when there is a read-only exception private int startX; - private Color editColor; - private Color separatorColor; private int unitByteSize; private FieldHighlightFactory highlightFactory; - FieldFactory(DataFormatModel model, int bytesPerLine, int fieldOffset, FontMetrics fm, - ByteViewerHighlighter highlightProvider) { + /** + * Constructor + * @param model data format model that knows how to represent the data + * @param fieldCount number of fields in a row + * @param label label that is used as a renderer in the field viewer + */ + FieldFactory(DataFormatModel model, int bytesPerLine, int fieldOffset, int charWidth, + FontMetrics fm, ByteViewerHighlighter highlightProvider) { this.model = model; this.fieldOffset = fieldOffset; this.fm = fm; this.highlightFactory = new SimpleHighlightFactory(highlightProvider); - charWidth = fm.charWidth('W'); + this.charWidth = charWidth; width = charWidth * model.getDataUnitSymbolSize(); - editColor = ByteViewerComponentProvider.EDITED_TEXT_COLOR; - separatorColor = ByteViewerComponentProvider.SEPARATOR_COLOR; unitByteSize = model.getUnitByteSize(); } @@ -98,9 +102,9 @@ class FieldFactory { ByteBlockInfo info = indexMap.getBlockInfo(index, fieldOffset); if (info == null) { if (indexMap.isBlockSeparatorIndex(index)) { - ByteField bf = new ByteField(noValueStr, fm, startX, width, false, fieldOffset, - index, highlightFactory); - bf.setForeground(separatorColor); + ByteField bf = new ByteField(noValueStr, fm, startX, width, charWidth, false, + fieldOffset, index, highlightFactory); + bf.setForeground(ByteViewerComponentProvider.SEPARATOR_COLOR); return bf; } return null; @@ -116,10 +120,10 @@ class FieldFactory { return getByteField(readErrorStr, index); } String str = model.getDataRepresentation(block, offset); - ByteField bf = - new ByteField(str, fm, startX, width, false, fieldOffset, index, highlightFactory); + ByteField bf = new ByteField(str, fm, startX, width, charWidth, false, fieldOffset, + index, highlightFactory); if (blockSet.isChanged(block, offset, unitByteSize)) { - bf.setForeground(editColor); + bf.setForeground(ByteViewerComponentProvider.EDITED_TEXT_COLOR); } return bf; } @@ -150,8 +154,8 @@ class FieldFactory { void setIndexMap(IndexMap indexMap) { this.indexMap = indexMap; if (indexMap != null) { - noValueStr = getString("."); - readErrorStr = getString("?"); + noValueStr = ".".repeat(model.getDataUnitSymbolSize()); + readErrorStr = "?".repeat(model.getDataUnitSymbolSize()); blockSet = indexMap.getByteBlockSet(); } else { @@ -174,32 +178,9 @@ class FieldFactory { return model.getColumnPosition(block, byteOffset); } - /** - * Set the color used to denote changes. - * @param c the color - */ - void setEditColor(Color c) { - editColor = c; - } - - void setSeparatorColor(Color c) { - separatorColor = c; - } - - /** - * Get the padded string that has the given char value. - */ - private String getString(String value) { - StringBuffer sb = new StringBuffer(); - int count = model.getDataUnitSymbolSize(); - for (int i = 0; i < count; i++) { - sb.append(value); - } - return sb.toString(); - } - private ByteField getByteField(String value, BigInteger index) { - return new ByteField(value, fm, startX, width, false, fieldOffset, index, highlightFactory); + return new ByteField(value, fm, startX, width, charWidth, false, fieldOffset, index, + highlightFactory); } static class SimpleHighlightFactory implements FieldHighlightFactory { diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FileByteBlock.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FileByteBlock.java index 02dfa7765d..72d94ea669 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FileByteBlock.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/FileByteBlock.java @@ -15,11 +15,12 @@ */ package ghidra.app.plugin.core.byteviewer; +import java.math.BigInteger; + import ghidra.app.plugin.core.format.ByteBlock; import ghidra.app.plugin.core.format.ByteBlockAccessException; -import ghidra.util.*; - -import java.math.BigInteger; +import ghidra.util.DataConverter; +import ghidra.util.LittleEndianDataConverter; /** * ByteBlock for a byte buffer read from a file. @@ -44,10 +45,7 @@ class FileByteBlock implements ByteBlock { @Override public String getLocationRepresentation(BigInteger bigIndex) { int index = bigIndex.intValue(); - if (index < buf.length) { - return pad(Integer.toString(index), 8); - } - return null; + return index < buf.length ? "%08d".formatted(index) : null; } @Override @@ -83,6 +81,18 @@ class FileByteBlock implements ByteBlock { return 0; } + @Override + public int getBytes(byte[] bytes, BigInteger bigIndex, int count) + throws ByteBlockAccessException { + int index = bigIndex.intValue(); + if (index < buf.length) { + count = Math.min(count, buf.length - index); + System.arraycopy(buf, index, bytes, 0, count); + return count; + } + return 0; + } + /* (non-Javadoc) * @see ghidra.app.plugin.core.format.ByteBlock#getShort(int) */ @@ -214,14 +224,4 @@ class FileByteBlock implements ByteBlock { byte[] getBytes() { return buf; } - - private String pad(String str, int length) { - StringBuffer sb = new StringBuffer(); - int nspaces = length - str.length(); - for (int i = 0; i < nspaces; i++) { - sb.append(" "); - } - sb.append(str); - return sb.toString(); - } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java index 22bd48bddb..b19f9a339d 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java @@ -38,7 +38,7 @@ class IndexFieldFactory { private int charWidth; private String noValueStr; private int startX; - private Color missingValueColor; + private Color missingValueColor = ByteViewerComponentProvider.SEPARATOR_COLOR; private FieldHighlightFactory highlightFactory = new DummyHighlightFactory(); /** @@ -46,15 +46,8 @@ class IndexFieldFactory { * @param metrics the FontMetrics this field should use to do size computations */ IndexFieldFactory(FontMetrics metrics) { - this.metrics = metrics; - - charWidth = metrics.charWidth('W'); - width = ByteViewerComponentProvider.DEFAULT_NUMBER_OF_CHARS * charWidth; - missingValueColor = ByteViewerComponentProvider.SEPARATOR_COLOR; - startX = charWidth; - - // initialize to a non-null value - setSize(1); + setFontMetrics(metrics); + setSize(ByteViewerComponentProvider.DEFAULT_NUMBER_OF_CHARS); } /** @@ -95,18 +88,6 @@ class IndexFieldFactory { return new SimpleTextField(locRep, metrics, startX, width, false, highlightFactory); } - public FontMetrics getMetrics() { - return metrics; - } - - /** - * Sets the starting x position for the fields generated by this factory - * @param x The - */ - public void setStartX(int x) { - startX = x; - } - /** * Returns the starting x position for the fields generated by this factory. * @return the starting x position for the fields generated by this factory. @@ -143,8 +124,12 @@ class IndexFieldFactory { noValueStr = StringUtils.repeat('.', charCount); } - void setMissingValueColor(Color c) { - missingValueColor = c; + void setFontMetrics(FontMetrics fm) { + this.metrics = fm; + + // 'W' is a good exemplar because addresses and address space names will typically be latin + charWidth = metrics.charWidth('W'); + startX = charWidth; } static class DummyHighlightFactory implements FieldHighlightFactory { diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java index 2d0455be28..5c0cfa2778 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -38,20 +38,13 @@ public class InteractivePanelManager { public InteractivePanelManager() { JTable table = new JTable(); header = table.getTableHeader(); + header.setFont(ByteViewerComponentProvider.HEADER_FONT); columnModel = header.getColumnModel(); separatorWidth = (new JSeparator(SwingConstants.VERTICAL)).getPreferredSize().width; mainPanel = new JPanel(new HeaderLayoutManager()); columnModel.addColumnModelListener(new PanelManagerColumnModelListener()); } - /** - * Sets the font for the header component. - * @param font the font to be used to display view names in the header - */ - public void setHeaderFont(Font font) { - header.setFont(font); - } - /** * Adds a component and it's name to the set of managed components. * @param name the name to display in its column header. @@ -297,7 +290,9 @@ public class InteractivePanelManager { record ComponentData(String name, JComponent component) { @Override public String toString() { - return name; + return component instanceof ByteViewerComponentNamer bvcn + ? bvcn.getByteViewerComponentName() + : name; } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/MemoryByteBlock.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/MemoryByteBlock.java index 55cd32c00b..7ccc418aee 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/MemoryByteBlock.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/MemoryByteBlock.java @@ -19,7 +19,6 @@ import java.math.BigInteger; import ghidra.app.plugin.core.format.ByteBlock; import ghidra.app.plugin.core.format.ByteBlockAccessException; -import ghidra.program.database.mem.ByteMappingScheme; import ghidra.program.model.address.*; import ghidra.program.model.listing.*; import ghidra.program.model.mem.*; @@ -33,7 +32,6 @@ public class MemoryByteBlock implements ByteBlock { private Memory memory; private Address start; private boolean bigEndian; - private Address mAddr; private Program program; /** @@ -47,9 +45,8 @@ public class MemoryByteBlock implements ByteBlock { this.program = program; this.memory = memory; this.block = block; - start = block.getStart(); - bigEndian = memory.isBigEndian(); - mAddr = start; + this.start = block.getStart(); + this.bigEndian = memory.isBigEndian(); } /** @@ -132,6 +129,17 @@ public class MemoryByteBlock implements ByteBlock { } } + @Override + public int getBytes(byte[] bytes, BigInteger index, int count) throws ByteBlockAccessException { + try { + Address addr = getAddress(index); + return memory.getBytes(addr, bytes, 0, count); + } + catch (MemoryAccessException e) { + throw new ByteBlockAccessException(e.getMessage()); + } + } + @Override public boolean hasValue(BigInteger index) { Address addr = getAddress(index); @@ -293,37 +301,22 @@ public class MemoryByteBlock implements ByteBlock { return (int) (start.getOffset() % radix); } - private Address getMappedAddress(Address addr) { - MemoryBlock memBlock = memory.getBlock(addr); - if (memBlock != null && memBlock.getType() == MemoryBlockType.BYTE_MAPPED) { - try { - MemoryBlockSourceInfo info = memBlock.getSourceInfos().get(0); - AddressRange mappedRange = info.getMappedRange().get(); - ByteMappingScheme byteMappingScheme = info.getByteMappingScheme().get(); - addr = byteMappingScheme.getMappedSourceAddress(mappedRange.getMinAddress(), - addr.subtract(memBlock.getStart())); - } - catch (AddressOverflowException e) { - // ignore - } - } - return addr; - } - /** * Get the address based on the index. */ public Address getAddress(BigInteger index) { try { - mAddr = start; - mAddr = mAddr.addNoWrap(index); - return mAddr; + return start.addNoWrap(index); } catch (AddressOverflowException e) { throw new IndexOutOfBoundsException("Index " + index + " is not in this block"); } } + public BigInteger getIndex(Address addr) { + return addr.getOffsetAsBigInteger().subtract(start.getOffsetAsBigInteger()); + } + /** * Indicates whether some other object is "equal to" this one. */ diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/OptionsAction.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/OptionsAction.java deleted file mode 100644 index 6ba6e1c197..0000000000 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/OptionsAction.java +++ /dev/null @@ -1,47 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.byteviewer; - -import javax.swing.Icon; - -import docking.ActionContext; -import docking.action.DockingAction; -import docking.action.ToolBarData; -import generic.theme.GIcon; -import ghidra.framework.plugintool.Plugin; -import ghidra.framework.plugintool.PluginTool; - -class OptionsAction extends DockingAction { - public static final Icon OPTIONS_ICON = new GIcon("icon.plugin.byteviewer.options"); - - private final ByteViewerComponentProvider provider; - - private final PluginTool tool; - - public OptionsAction(ByteViewerComponentProvider provider, Plugin plugin) { - super("Byte Viewer Options", plugin.getName()); - this.provider = provider; - this.tool = plugin.getTool(); - setEnabled(false); - setDescription("Set Byte Viewer Options"); - setToolBarData(new ToolBarData(OPTIONS_ICON, "ZSettings")); - } - - @Override - public void actionPerformed(ActionContext context) { - tool.showDialog(new ByteViewerOptionsDialog(provider), provider); - } -} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteBlockSet.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteBlockSet.java index 54bf0b5c4c..13422a1bae 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteBlockSet.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteBlockSet.java @@ -28,6 +28,7 @@ import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramSelection; +import ghidra.util.NumericUtilities; /** * ByteBlockSet implementation for a Program object. @@ -238,12 +239,7 @@ public class ProgramByteBlockSet implements ByteBlockSet { try { long off = address.subtract(memBlocks[i].getStart()); - BigInteger offset = - (off < 0) - ? BigInteger.valueOf(off + 0x8000000000000000L) - .subtract( - BigInteger.valueOf(0x8000000000000000L)) - : BigInteger.valueOf(off); + BigInteger offset = NumericUtilities.unsignedLongToBigInteger(off); return new ByteBlockInfo(blocks[i], offset); } catch (Exception e) { diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteViewerComponentProvider.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteViewerComponentProvider.java index 2de28c5a30..5ac2fd0e80 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteViewerComponentProvider.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ProgramByteViewerComponentProvider.java @@ -27,7 +27,7 @@ import javax.swing.*; import docking.ActionContext; import docking.DockingUtils; -import docking.action.*; +import docking.action.builder.ActionBuilder; import docking.widgets.fieldpanel.support.ViewerPosition; import generic.theme.GIcon; import ghidra.app.events.*; @@ -59,8 +59,6 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi private WeakSet navigationListeners = WeakDataStructureFactory.createCopyOnWriteWeakSet(); - private CloneByteViewerAction cloneByteViewerAction; - protected Program program; protected ProgramSelection currentSelection; protected ProgramSelection liveSelection; @@ -105,8 +103,16 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi } public void createProgramActions() { - cloneByteViewerAction = new CloneByteViewerAction(); - tool.addLocalAction(this, cloneByteViewerAction); + new ActionBuilder("ByteViewer Clone", plugin.getName()) + .toolBarIcon(new GIcon("icon.provider.clone")) + .toolBarGroup("ZZZ") + .description("Create a snapshot (disconnected) copy of this Bytes window ") + .helpLocation(new HelpLocation("Snapshots", "Snapshots_Start")) + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_T, + DockingUtils.CONTROL_KEY_MODIFIER_MASK | InputEvent.SHIFT_DOWN_MASK)) + .enabledWhen(ac -> blockSet != null && blockSet.isValid()) + .onAction(ac -> cloneWindow()) + .buildAndInstallLocal(this); } @Override @@ -150,7 +156,7 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi } protected ByteViewerActionContext newByteViewerActionContext() { - return new ByteViewerActionContext(this); + return new ByteViewerActionContext(this, panel.getCurrentComponent()); } @Override @@ -233,9 +239,6 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi } protected void doSetProgram(Program newProgram) { - setOptionsAction.setEnabled(newProgram != null); - cloneByteViewerAction.setEnabled(newProgram != null); - if (program != null) { program.removeListener(this); } @@ -244,8 +247,8 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi clipboardProvider.setProgram(newProgram); for (ByteViewerComponent byteViewerComponent : viewMap.values()) { DataFormatModel dataModel = byteViewerComponent.getDataModel(); - if (dataModel instanceof ProgramDataFormatModel) { - ((ProgramDataFormatModel) dataModel).setProgram(newProgram); + if (dataModel instanceof ProgramDataFormatModel pdfm) { + pdfm.setProgram(newProgram); } } @@ -254,6 +257,7 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi } setByteBlocks(null); updateTitle(); + contextChanged(); } protected void updateTitle() { @@ -615,9 +619,12 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi @Override protected void updateLiveSelection(ByteBlockSelection selection) { - AbstractSelectionPluginEvent event = blockSet.getPluginEvent(plugin.getName(), selection); - liveSelection = event.getSelection(); - updateTitle(); + if (blockSet != null) { + AbstractSelectionPluginEvent event = + blockSet.getPluginEvent(plugin.getName(), selection); + liveSelection = event.getSelection(); + updateTitle(); + } } @Override @@ -760,29 +767,6 @@ public class ProgramByteViewerComponentProvider extends ByteViewerComponentProvi newProvider.panel.setViewerPosition(viewerPosition); } -//================================================================================================== -// Inner Classes -//================================================================================================== - - private class CloneByteViewerAction extends DockingAction { - - public CloneByteViewerAction() { - super("ByteViewer Clone", plugin.getName()); - Icon image = new GIcon("icon.provider.clone"); - setToolBarData(new ToolBarData(image, "ZZZ")); - - setDescription("Create a snapshot (disconnected) copy of this Bytes window "); - setHelpLocation(new HelpLocation("Snapshots", "Snapshots_Start")); - setKeyBindingData(new KeyBindingData(KeyEvent.VK_T, - DockingUtils.CONTROL_KEY_MODIFIER_MASK | InputEvent.SHIFT_DOWN_MASK)); - } - - @Override - public void actionPerformed(ActionContext context) { - cloneWindow(); - } - } - @Override public void addNavigatableListener(NavigatableRemovalListener listener) { navigationListeners.add(listener); diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ToggleEditAction.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ToggleEditAction.java deleted file mode 100644 index d4d3e0403c..0000000000 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ToggleEditAction.java +++ /dev/null @@ -1,47 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.byteviewer; - -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; - -import docking.ActionContext; -import docking.DockingUtils; -import docking.action.*; -import generic.theme.GIcon; -import ghidra.framework.plugintool.Plugin; - -class ToggleEditAction extends ToggleDockingAction { - private final ByteViewerComponentProvider provider; - - public ToggleEditAction(ByteViewerComponentProvider provider, Plugin plugin) { - super("Enable/Disable Byteviewer Editing", plugin.getName()); - this.provider = provider; - setToolBarData(new ToolBarData(new GIcon("icon.base.edit.bytes"), "Byteviewer")); - setKeyBindingData(new KeyBindingData( - KeyEvent.VK_E, DockingUtils.CONTROL_KEY_MODIFIER_MASK | InputEvent.ALT_DOWN_MASK)); - - setDescription("Enable/Disable editing of bytes in Byte Viewer panels."); - setSelected(false); - setEnabled(true); - } - - @Override - public void actionPerformed(ActionContext context) { - boolean isSelected = isSelected(); - provider.setEditMode(isSelected); - } -} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AddressFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AddressFormatModel.java index 85f0e6fe69..dbe616dcc4 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AddressFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AddressFormatModel.java @@ -15,6 +15,8 @@ */ package ghidra.app.plugin.core.format; +import java.math.BigInteger; + import ghidra.app.plugin.core.byteviewer.MemoryByteBlock; import ghidra.program.model.address.*; import ghidra.program.model.listing.Listing; @@ -23,8 +25,6 @@ import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryAccessException; import ghidra.util.HelpLocation; -import java.math.BigInteger; - /** * Converts byte values to Ascii representation. */ @@ -177,31 +177,6 @@ public class AddressFormatModel implements ProgramDataFormatModel { return true; } - /** - * Returns true if the formatter allows values to be changed. - */ - @Override - public boolean isEditable() { - return false; - } - - /** - * Overwrite a value in a ByteBlock. - * @param block block to change - * @param index byte index into the block - * @param pos The position within the unit where c will be the - * new character. - * @param c new character to put at pos param - * @return true if the replacement is legal, false if the - * replacement value would not make sense for this format, e.g. - * attempt to put a 'z' in a hex unit. - * block - */ - @Override - public boolean replaceValue(ByteBlock block, BigInteger index, int charPosition, char c) { - return false; - } - /** * Get the number of characters separating units. */ @@ -210,34 +185,6 @@ public class AddressFormatModel implements ProgramDataFormatModel { return 0; } - /** - * Get number of units in a group. A group may represent - * multiple units shown as one entity. This format does not - * support groups. - */ - @Override - public int getGroupSize() { - return 0; - } - - /** - * Set the number of units in a group. This format does not - * support groups. - * @throws UnsupportedOperationException - */ - @Override - public void setGroupSize(int groupSize) { - throw new UnsupportedOperationException("groups are not supported"); - } - - /** - * @see ghidra.app.plugin.core.format.DataFormatModel#validateBytesPerLine(int) - */ - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return true; - } - /** * Set the program. This formatter is dependent upon listing from program. * There are two cases where this depedency only appears. All Formatters that diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AsciiFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AsciiFormatModel.java deleted file mode 100644 index 10246fb473..0000000000 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/AsciiFormatModel.java +++ /dev/null @@ -1,197 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.plugin.core.format; - -import ghidra.app.plugin.core.byteviewer.ByteViewerComponentProvider; -import ghidra.util.HelpLocation; - -import java.math.BigInteger; - -/** - * Converts byte values to Ascii representation. - */ -public class AsciiFormatModel implements UniversalDataFormatModel { - - private int symbolSize; - - public AsciiFormatModel() { - symbolSize = 1; - } - - /** - * Get the name of this formatter. - */ - @Override - public String getName() { - return "Ascii"; - } - - /** - * Get the number of bytes to make a unit; in this case it - * takes 1 byte to make an Ascii value. - */ - @Override - public int getUnitByteSize() { - return 1; - } - - /** - * Given a character position from 0 to data unit symbol size - 1 - * it returns a number from 0 to unit byte size - 1 indicating which - * byte the character position was obtained from. - */ - @Override - public int getByteOffset(ByteBlock block, int position) { - return 0; - } - - /** - * Given the byte offset into a unit, get the column position. - */ - @Override - public int getColumnPosition(ByteBlock block, int byteOffset) { - return 0; - } - - /** - * Gets the number of characters required to display a - * unit. - */ - @Override - public int getDataUnitSymbolSize() { - return symbolSize; - } - - /** - * Gets the string representation at the given index in the block. - * @param block block to change - * @param index byte index into the block - * @throws ByteBlockAccessException if the block cannot be read - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ - @Override - public String getDataRepresentation(ByteBlock block, BigInteger index) - throws ByteBlockAccessException { - - byte b = block.getByte(index); - String str = null; - if (b < 0x20 || b == 0x7f) { - str = "."; - } - else { - char[] charArray = { (char) b }; - str = new String(charArray); - } - - return (str); - - } - - /** - * Returns true if the formatter allows values to be changed. - */ - @Override - public boolean isEditable() { - return true; - } - - /** - * Overwrite a value in a ByteBlock. - * @param block block to change - * @param index byte index into the block - * @param pos The position within the unit where c will be the - * new character. - * @param c new character to put at pos param - * @return true if the replacement is legal, false if the - * replacement value would not make sense for this format, e.g. - * attempt to put a 'z' in a hex unit. - * @throws ByteBlockAccessException if the block cannot be updated - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ - @Override - public boolean replaceValue(ByteBlock block, BigInteger index, int charPosition, char c) - throws ByteBlockAccessException { - - if (charPosition != 0) { - return false; - } - - block.getByte(index); - byte cb = (byte) c; - - if (cb < 0x20 || cb == 0x7f) { - return false; - } - - block.setByte(index, cb); - return true; - } - - /** - * Get number of units in a group. A group may represent - * multiple units shown as one entity. This format does not - * support groups. - * @throws UnsupportedOperationException - */ - @Override - public int getGroupSize() { - return 0; - } - - /** - * Set the number of units in a group. This format does not - * support groups. - * @throws UnsupportedOperationException - */ - @Override - public void setGroupSize(int groupSize) { - throw new UnsupportedOperationException("groups are not supported"); - } - - /** - * Get the number of characters separating units. - */ - @Override - public int getUnitDelimiterSize() { - return 0; - } - - /** - * @see ghidra.app.plugin.core.format.DataFormatModel#validateBytesPerLine(int) - */ - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return true; - } - - /* (non-Javadoc) - * @see ghidra.app.plugin.format.DataFormatModel#getHelpLocation() - */ - @Override - public HelpLocation getHelpLocation() { - return new HelpLocation("ByteViewerPlugin", "Ascii"); - } - - @Override - public void dispose() { - } - - public boolean supportsProvider(ByteViewerComponentProvider provider) { - return true; - } -} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/BinaryFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/BinaryFormatModel.java index 15587231fa..2b1c1f91cb 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/BinaryFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/BinaryFormatModel.java @@ -15,14 +15,14 @@ */ package ghidra.app.plugin.core.format; -import ghidra.util.HelpLocation; - import java.math.BigInteger; +import ghidra.util.HelpLocation; + /** * Converts byte values to binary representation. */ -public class BinaryFormatModel implements UniversalDataFormatModel { +public class BinaryFormatModel implements UniversalDataFormatModel, MutableDataFormatModel { private int symbolSize; private static final String GOOD_CHARS = "01"; @@ -99,18 +99,10 @@ public class BinaryFormatModel implements UniversalDataFormatModel { str = str.substring(str.length() - symbolSize); } - return pad(str); + return DataFormatModel.pad(str, symbolSize); } - /** - * Returns true to allow values to be changed. - */ - @Override - public boolean isEditable() { - return (true); - } - /** * Overwrite a value in a ByteBlock. * @param block block to change @@ -154,26 +146,6 @@ public class BinaryFormatModel implements UniversalDataFormatModel { return true; } - /** - * Get number of units in a group. A group may represent - * multiple units shown as one entity. This format does not - * support groups. - */ - @Override - public int getGroupSize() { - return 1; - } - - /** - * Set the number of units in a group. This format does not - * support groups. - * @throws UnsupportedOperationException - */ - @Override - public void setGroupSize(int groupSize) { - throw new UnsupportedOperationException("groups are not supported"); - } - /** * Get the number of characters separating units. */ @@ -182,38 +154,8 @@ public class BinaryFormatModel implements UniversalDataFormatModel { return 1; } - /** - * @see ghidra.app.plugin.core.format.DataFormatModel#validateBytesPerLine(int) - */ - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return true; - } - - ///////////////////////////////////////////////////////////////// - // *** private methods *** - ///////////////////////////////////////////////////////////////// - private String pad(String value) { - StringBuffer sb = new StringBuffer(); - int len = symbolSize - value.length(); - - for (int i = 0; i < len; i++) { - sb.append("0"); - } - sb.append(value); - return (sb.toString()); - } - - /* (non-Javadoc) - * @see ghidra.app.plugin.format.DataFormatModel#getHelpLocation() - */ @Override public HelpLocation getHelpLocation() { return new HelpLocation("ByteViewerPlugin", "Binary"); } - - @Override - public void dispose() { - // nothing to do - } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlock.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlock.java index 12cb8656d1..5bf425a7dc 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlock.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlock.java @@ -51,6 +51,17 @@ public interface ByteBlock { */ public byte getByte(BigInteger index) throws ByteBlockAccessException; + /** + * Get bytes from given index. + * + * @param bytes destination + * @param index byte index + * @param count number of bytes to get + * @return actual number of bytes copied into destination + * @throws ByteBlockAccessException if error + */ + public int getBytes(byte[] bytes, BigInteger index, int count) throws ByteBlockAccessException; + /** * Returns true if this ByteBlock has byte values at the specified index. * diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlockSet.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlockSet.java index 553a0b98ae..43eab8a70c 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlockSet.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/ByteBlockSet.java @@ -25,6 +25,14 @@ import ghidra.program.model.address.AddressSet; * Interface to define methods for getting byte blocks and translating events. */ public interface ByteBlockSet { + + /** + * {@return true if this instance represents a valid source of data, false if this + * instance does not represent a valid source of data} + */ + default public boolean isValid() { + return true; + } /** * Get the blocks in this set. diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CharacterFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CharacterFormatModel.java new file mode 100644 index 0000000000..b0b47600c1 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CharacterFormatModel.java @@ -0,0 +1,236 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.format; + +import java.awt.FontMetrics; +import java.lang.Character.UnicodeScript; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import docking.ActionContext; +import docking.Tool; +import docking.action.DockingActionIf; +import docking.action.builder.ToggleActionBuilder; +import docking.actions.PopupActionProvider; +import ghidra.app.plugin.core.byteviewer.*; +import ghidra.program.model.lang.Endian; +import ghidra.util.*; +import ghidra.util.charset.CharsetInfo; +import ghidra.util.charset.CharsetInfoManager; + +/** + * Converts byte values to Character representation. (previously the AsciiFormatModel) + */ +public class CharacterFormatModel implements UniversalDataFormatModel, MutableDataFormatModel, + PopupActionProvider, CursorWidthDataFormatModel, TooltipDataFormatModel { + + public final static String NAME = "Chars"; + + private CharsetInfo csi = CharsetInfoManager.getInstance().get(StandardCharsets.US_ASCII); + private Charset cs = StandardCharsets.US_ASCII; + private int maxBytesPerChar = 1; + private boolean compactChars = true; + private int bytesPerChar = 1; + + public CharacterFormatModel() { + } + + @Override + public void setByteViewerConfigOptions(ByteViewerConfigOptions options) { + this.csi = options.getCharsetInfo(); + this.cs = csi.getCharset(); + this.maxBytesPerChar = Math.max(csi.getMaxBytesPerChar(), 1); + this.compactChars = options.isCompactChars(); + this.bytesPerChar = + options.isUseCharAlignment() && csi.getAlignment() > 1 ? csi.getAlignment() : 1; + + } + + @Override + public int getCursorWidth(FontMetrics fm) { + return fm.charWidth('W') * (compactChars ? 1 : 2); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescriptiveName() { + return NAME + " (%s)".formatted(cs.name()); + } + + @Override + public int getUnitByteSize() { + return bytesPerChar; + } + + @Override + public int getByteOffset(ByteBlock block, int position) { + return 0; + } + + @Override + public int getColumnPosition(ByteBlock block, int byteOffset) { + return 0; + } + + @Override + public int getDataUnitSymbolSize() { + return 1; + } + + @Override + public String getDataRepresentation(ByteBlock block, BigInteger index) + throws ByteBlockAccessException { + Integer codePoint = getCodePointAt(block, index); + if (codePoint == null) { + return "?"; + } + int cp = codePoint.intValue(); + if (cp == StringUtilities.UNICODE_REPLACEMENT || Character.isISOControl(cp) || + !Character.isValidCodePoint(cp)) { + return "."; + } + return Character.toString(cp); + } + + private Integer getCodePointAt(ByteBlock block, BigInteger index) + throws ByteBlockAccessException { + // create buffer with as many bytes as it may take to read the largest + // encoded character using the current charset. + // This may give us more than 1 character when decoded, we throw away anything + // other than the first char. + + // Future: would be nice to not have to call getAdjustedCS() for each fetch operation + Charset bomCS = getAdjustedCS(block); + byte[] bytes = new byte[maxBytesPerChar]; + int byteCount = block.getBytes(bytes, index, maxBytesPerChar); + String s = new String(bytes, 0, byteCount, bomCS); + if (s.isEmpty()) { + return null; + } + return s.codePointAt(0); + } + + private Charset getAdjustedCS(ByteBlock block) { + // returns an alternate charset that fits the endianness of the memory + // to avoid spurious BOM bytes being emitted and incorrect + // assumption about how to decode bytes + if (CharsetInfoManager.isBOMCharset(csi.getName())) { + Endian endian = block.isBigEndian() ? Endian.BIG : Endian.LITTLE; + CharsetInfo bomCSI = + CharsetInfoManager.getInstance().get(csi.getName() + endian.toShortString()); + return bomCSI != null ? bomCSI.getCharset() : cs; + } + return cs; + } + + private byte[] getBytesForCodePoint(int cp, Charset bomCS) { + String s = Character.toString(cp); + if (bomCS.canEncode() && bomCS.newEncoder().canEncode(s)) { + ByteBuffer bb = bomCS.encode(s); + byte[] bytes = new byte[bb.limit()]; + bb.get(bytes); + return bytes; + } + return null; + } + + @Override + public boolean replaceValue(ByteBlock block, BigInteger index, int charPosition, char c) + throws ByteBlockAccessException { + + if (charPosition != 0) { + return false; + } + + block.getByte(index); + byte cb = (byte) c; + + // right now only supports US-ASCII when replacing values + if (cb < 0x20 || cb == 0x7f) { + return false; + } + + block.setByte(index, cb); + return true; + } + + @Override + public int getUnitDelimiterSize() { + return 0; + } + + @Override + public HelpLocation getHelpLocation() { + return new HelpLocation("ByteViewerPlugin", "Chars"); + } + + @Override + public List getPopupActions(Tool tool, ActionContext context) { + return List.of(new ToggleActionBuilder("CompactCharWidth", "ByteViewerPlugin") + .selected(compactChars) + .withContext(ByteViewerActionContext.class) + .popupMenuPath("Compact/Wide Layout") + .onAction(ac -> ac.getComponentProvider().setCompactChars(!compactChars)) + .helpLocation(new HelpLocation("ByteViewerPlugin", "CompactCharWidth")) + .build()); + } + + @Override + public String getTooltip(ByteBlock block, BigInteger index, ByteViewerComponent comp) { + try { + Integer cp; + if ((cp = getCodePointAt(block, index)) != null) { + Charset bomCS = getAdjustedCS(block); + byte[] bytes = getBytesForCodePoint(cp, bomCS); + boolean canDisplay = comp.getFont().canDisplay(cp); + UnicodeScript script = UnicodeScript.of(cp); + String charRep = + canDisplay && !Character.isISOControl(cp) ? Character.toString(cp) : "NA"; + String bytesRep = bytes != null + ? NumericUtilities.convertBytesToString(bytes, " ") + : "unavailable"; + + String s = """ + + Character info:
    + + + %s +
    CharUnicodeScript
    0x%04x%s
    +


    +
    + %s Bytes: %s
    + %s +
    + """.formatted(charRep, cp, script, bomCS.name(), bytesRep, + !canDisplay ? "
    (unrenderable)" : ""); + return s; + } + } + catch (ByteBlockAccessException e) { + // ignore + } + return null; + } + +} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CursorWidthDataFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CursorWidthDataFormatModel.java new file mode 100644 index 0000000000..90acfcdfa2 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/CursorWidthDataFormatModel.java @@ -0,0 +1,26 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.format; + +import java.awt.FontMetrics; + +/** + * Optional interface for DataFormatModels that allows the model to specify the width of individual + * characters and the cursor needed to display those characters + */ +public interface CursorWidthDataFormatModel extends DataFormatModel { + int getCursorWidth(FontMetrics fm); +} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DataFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DataFormatModel.java index d1b369ae6a..5a4694228c 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DataFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DataFormatModel.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,11 +15,12 @@ */ package ghidra.app.plugin.core.format; +import java.math.BigInteger; + +import ghidra.app.plugin.core.byteviewer.ByteViewerConfigOptions; import ghidra.util.HelpLocation; import ghidra.util.classfinder.ExtensionPoint; -import java.math.BigInteger; - /** * NOTE: ALL DataFormatModel CLASSES MUST END IN "FormatModel". If not, * the ClassSearcher will not find them. @@ -30,24 +30,28 @@ import java.math.BigInteger; */ public interface DataFormatModel extends ExtensionPoint { - public static final int NEXT_UNIT = -1; - public static final int PREVIOUS_UNIT = -1; - /** * Gets the number of bytes to make a unit, e.g., * for 'byte' unit size =1, for 'unicode' unit size = 2, etc. */ - public int getUnitByteSize(); + int getUnitByteSize(); /** * Gets data format name. */ - public String getName(); + String getName(); + + /** + * {@return a descriptive name for this data format, used for labels / headers} + */ + default String getDescriptiveName() { + return getName(); + } /** * Gets the help location for this format */ - public HelpLocation getHelpLocation(); + HelpLocation getHelpLocation(); /** * Gets the number of characters required to display a @@ -55,19 +59,19 @@ public interface DataFormatModel extends ExtensionPoint { * may display a unit as '0xff'. The data unit * size returned would be 4. */ - public int getDataUnitSymbolSize(); + int getDataUnitSymbolSize(); /** * Given a character position from 0 to data unit symbol size - 1 * it returns a number from 0 to unit byte size - 1 indicating which * byte the character position was obtained from. */ - public int getByteOffset(ByteBlock block, int position); + int getByteOffset(ByteBlock block, int position); /** * Given the byte offset into a unit, get the column position. */ - public int getColumnPosition(ByteBlock block, int byteOffset); + int getColumnPosition(ByteBlock block, int byteOffset); /** * Gets the string representation at the given index in the block. @@ -77,56 +81,37 @@ public interface DataFormatModel extends ExtensionPoint { * @throws IndexOutOfBoundsException if index is not valid for the * block */ - public String getDataRepresentation(ByteBlock block, BigInteger index) - throws ByteBlockAccessException; + String getDataRepresentation(ByteBlock block, BigInteger index) throws ByteBlockAccessException; + + default void setByteViewerConfigOptions(ByteViewerConfigOptions options) { + // default do-nothing + } /** - * Returns true if the formatter allows values to be changed. + * Returns an error message string if the supplied {@link ByteViewerConfigOptions} are + * problematic, otherwise returns null. + * + * @param candidateOptions {@link ByteViewerConfigOptions} + * @return null if candidate config options are ok, otherwise error message string */ - public boolean isEditable(); - - /** - * Overwrite a value in a ByteBlock. - * @param block block to change - * @param index byte index into the block - * @param pos The position within the unit where c will be the - * new character. - * @param c new character to put at pos param - * @return true if the replacement is legal, false if the - * replacement value would not make sense for this format, e.g. - * attempt to put a 'z' in a hex unit. - * @throws ByteBlockAccessException if the block cannot be updated - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ - public boolean replaceValue(ByteBlock block, BigInteger index, int pos, char c) - throws ByteBlockAccessException; - - /** - * Get number of units in a group. A group may represent - * multiple units shown as one entity. - */ - public int getGroupSize(); - - /** - * Set the number of units in a group. - * @throws UnsupportedOperationException if model does not - * support groups - */ - public void setGroupSize(int groupSize); + default String validateByteViewerConfigOptions(ByteViewerConfigOptions candidateOptions) { + return null; + } /** * Get the number of characters separating units. */ - public int getUnitDelimiterSize(); + int getUnitDelimiterSize(); - /** - * Verify that this model can support the given bytes per line - * value. - * @return true if this model supports the given number of bytes per line - */ - public boolean validateBytesPerLine(int bytesPerLine); + default void dispose() { + // do nothing by default + } - public void dispose(); + static String pad(String value, int symbolSize) { + return pad(value, symbolSize, "0"); + } + static String pad(String value, int symbolSize, String padChar) { + return padChar.repeat(Math.max(symbolSize - value.length(), 0)) + value; + } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DisassembledFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DisassembledFormatModel.java index 1482ef88ec..9a7f1c1228 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DisassembledFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/DisassembledFormatModel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -127,65 +127,6 @@ public class DisassembledFormatModel implements ProgramDataFormatModel { return str; } - /** - * Returns true if the formatter allows values to be changed. - */ - @Override - public boolean isEditable() { - return false; - } - - /** - * Overwrite a value in a ByteBlock. - * @param block block to change - * @param index byte index into the block - * @param pos The position within the unit where c will be the - * new character. - * @param c new character to put at pos param - * @return true if the replacement is legal, false if the - * replacement value would not make sense for this format, e.g. - * attempt to put a 'z' in a hex unit. - * @throws ByteBlockAccessException if the block cannot be updated - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ - @Override - public boolean replaceValue(ByteBlock block, BigInteger index, int charPosition, char c) - throws ByteBlockAccessException { - - if (charPosition != 0) { - return false; - } - - byte cb = (byte) c; - - if (cb < 0x20 || cb == 0x7f) { - return (false); - } - - block.setByte(index, cb); - return true; - } - - /** - * Get number of units in a group. A group may represent - * multiple units shown as one entity. This format does not - * support groups. - */ - @Override - public int getGroupSize() { - return 0; - } - - /** - * Set the number of units in a group. This format does not - * support groups. - * @throws UnsupportedOperationException - */ - @Override - public void setGroupSize(int groupSize) { - throw new UnsupportedOperationException("groups are not supported"); - } /** * Get the number of characters separating units. @@ -195,14 +136,6 @@ public class DisassembledFormatModel implements ProgramDataFormatModel { return 0; // no space between units } - /** - * @see ghidra.app.plugin.core.format.DataFormatModel#validateBytesPerLine(int) - */ - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return true; - } - /** * Set the program. This formatter is dependent upon listing from program. * There are two cases where this dependency only appears. All Formatters that diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexFormatModel.java index 8bdf8f91e8..acbb8eb315 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexFormatModel.java @@ -15,252 +15,97 @@ */ package ghidra.app.plugin.core.format; -import ghidra.util.HelpLocation; - import java.math.BigInteger; +import ghidra.app.plugin.core.byteviewer.ByteViewerConfigOptions; +import ghidra.util.HelpLocation; + /** - * Converts byte values to hex representation. + * Converts byte values to hex representation, optionally grouping the byte values + * together in groups of {@link #getHexGroupSize()}. */ -public class HexFormatModel implements UniversalDataFormatModel { +public class HexFormatModel implements UniversalDataFormatModel, MutableDataFormatModel { public final static String NAME = "Hex"; - private int symbolSize; - private int unitByteSize; - private boolean prefixEnabled; - private boolean alphaCapsEnabled; + private int symbolSize = 2; private int groupSize = 1; + private String fullSymbolErrorStr = "??"; private static final String GOOD_CHARS = "0123456789abcdefABCDEF"; public HexFormatModel() { - this.prefixEnabled = false; - this.alphaCapsEnabled = false; - unitByteSize = groupSize; - if (prefixEnabled) { - symbolSize = 4 * groupSize; // there are 2 chars per byte of data - } - else { - symbolSize = 2 * groupSize; // there are 2 chars per byte of data - } } - /** - * Get the name of this formatter. - */ @Override public String getName() { return NAME; } - /** - * Get the number of bytes to make a unit; in this case, - * returns 1. - */ @Override public int getUnitByteSize() { - return unitByteSize; + return groupSize; } - /** - * Gets the number of characters required to display a - * unit. - * @return 2 for number of characters in a unit - */ @Override public int getDataUnitSymbolSize() { return symbolSize; } - /** - * Get number of units in a group. - */ @Override - public int getGroupSize() { + public void setByteViewerConfigOptions(ByteViewerConfigOptions options) { + groupSize = options.getHexGroupSize(); + symbolSize = 2 * groupSize; + fullSymbolErrorStr = "??".repeat(groupSize); + } + + @Override + public String validateByteViewerConfigOptions(ByteViewerConfigOptions candidateOptions) { + // we can't rely on the caller to pre-check our validity based on + // groupsize % bytes_per_line because our groupsize will change after the configoptions + // are set. + if (candidateOptions.getBytesPerLine() % candidateOptions.getHexGroupSize() != 0) { + return "Hex (%d bytes) is not a multiple of %d".formatted( + candidateOptions.getHexGroupSize(), candidateOptions.getBytesPerLine()); + } + return null; + } + + public int getHexGroupSize() { return groupSize; } - /** - * Set the number of units in a group. - */ - @Override - public void setGroupSize(int groupSize) { - this.groupSize = groupSize; - unitByteSize = groupSize; - symbolSize = (2 * groupSize); - } - - /** - * Should this model display spaces by groupSize? - */ - public boolean isSpaceByGroupSize() { - return false; - } - - /** - * Returns the byte used to generate the character at a given - * position. - */ @Override public int getByteOffset(ByteBlock block, int pos) { - if (prefixEnabled) { - if (pos <= 3) { - return 0; - } - else if (pos < (2 + unitByteSize * 2)) { - return ((pos - 2) / 2); - } - return unitByteSize - 1; - } - if (pos < unitByteSize * 2) { - return (pos / 2); - } - return unitByteSize - 1; + return pos < symbolSize ? pos / 2 : groupSize - 1; } - /** - * Given the byte offset into a unit, get the column position. - */ @Override public int getColumnPosition(ByteBlock block, int byteOffset) { - if (prefixEnabled) { - return byteOffset * 2 + 2; - } return byteOffset * 2; } - /** - * Gets the position where the cursor should be placed in - * order to do an 'overwrite' of data. - * @param position current position of the cursor within - * the data unit. - */ - public int getInsertionPosition(int pos) { - if (prefixEnabled) { - if (pos <= 2) { - return 2; - } - else if (pos < symbolSize) { - return pos; - } - else { - return symbolSize - 1; - } - } - if (pos <= 0) { - return 0; - } - else if (pos < symbolSize) { - return pos; - } - else { - return symbolSize - 1; - } - } - - /** - * Gets the string representation at the given index in the block. - * @param block block to change - * @param index byte index into the block - * @throws ByteBlockAccessException if the block cannot be read - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ @Override public String getDataRepresentation(ByteBlock block, BigInteger index) throws ByteBlockAccessException { - StringBuffer sb = new StringBuffer(); - //System.out.println("representation: = " + block.getLocationRepresentation(index)+ ", index = " + index); - if (prefixEnabled) { - sb.append("0x"); + byte[] bytes = new byte[groupSize]; + int bytesRead = block.getBytes(bytes, index, groupSize); + if (bytesRead == 0) { + return fullSymbolErrorStr; } - - StringBuffer strBuff = new StringBuffer(); - BigInteger byteIndex = index; - boolean qflag = false; - for (int idx = 0; idx < unitByteSize; idx++) { - try { - byte b = block.getByte(byteIndex); - strBuff.append(adjust(Integer.toHexString(b))); - byteIndex = byteIndex.add(BigInteger.ONE); - } - catch (ByteBlockAccessException bbae) { - if (idx == 0 || qflag) { - strBuff.append("??"); - qflag = true; - } - } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytesRead; i++) { + sb.append("%02x".formatted(Byte.toUnsignedInt(bytes[i]))); } - - String str = strBuff.toString(); - - if (alphaCapsEnabled) { - str = str.toUpperCase(); - } - sb.append(str); - return new String(sb); + return sb.toString(); } - /** - * Gets the string representation for numUnits at the given - * index in the block. - * @param block block to change - * @param index byte index into the block - * @param numUnits number of units to get - * @throws ByteBlockAccessException if the block cannot be read - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ - public String getDataRepresentation(ByteBlock block, BigInteger index, int numUnits) - throws ByteBlockAccessException { - - int n; - StringBuffer sb = new StringBuffer(); - - System.out.println("representation: = " + block.getLocationRepresentation(index) + - ", index = " + index); - for (n = 0; n < numUnits; n++, index = index.add(BigInteger.ONE)) { - String str = getDataRepresentation(block, index); - - sb.append(str); - - } - - return new String(sb); - } - - /** - * Returns true to allow values to be changed. - */ - @Override - public boolean isEditable() { - return true; - } - - /** - * Get the number of characters separating units for display purposes. - */ @Override public int getUnitDelimiterSize() { return 1; } - /** - * Overwrite a value in a ByteBlock. - * @param block block to change - * @param index byte index into the block - * @param pos The position within the unit where c will be the - * new character. - * @param c new character to put at pos param - * @return true if the replacement is legal, false if the - * replacement value would not make sense for this format, e.g. - * attempt to put a 'z' in a hex unit. - * @throws ByteBlockAccessException if the block cannot be updated - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ @Override public boolean replaceValue(ByteBlock block, BigInteger index, int charPosition, char c) throws ByteBlockAccessException { @@ -268,8 +113,7 @@ public class HexFormatModel implements UniversalDataFormatModel { if (GOOD_CHARS.indexOf(c) == -1) { return false; } - if ((prefixEnabled && (charPosition < 2 || charPosition >= symbolSize)) || - (!prefixEnabled && (charPosition < 0 || charPosition >= symbolSize))) { + if (charPosition < 0 || charPosition >= symbolSize) { return false; } @@ -294,45 +138,9 @@ public class HexFormatModel implements UniversalDataFormatModel { return true; } - /** - * @see ghidra.app.plugin.core.format.DataFormatModel#validateBytesPerLine(int) - */ - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return true; - } - - ///////////////////////////////////////////////////////////////// - // *** private methods *** - ///////////////////////////////////////////////////////////////// - private String adjust(String value) { - StringBuffer sb = new StringBuffer(); - int strLen = value.length(); - - if (strLen > 2) { - sb.append(value.substring(strLen - 2)); - } - else { - int len = 2 - strLen; - - for (int i = 0; i < len; i++) { - sb.append("0"); - } - sb.append(value); - } - return (sb.toString()); - } - - /* (non-Javadoc) - * @see ghidra.app.plugin.format.DataFormatModel#getHelpLocation() - */ @Override public HelpLocation getHelpLocation() { return new HelpLocation("ByteViewerPlugin", "Hex"); } - @Override - public void dispose() { - } - } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexIntegerFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexIntegerFormatModel.java index a8f00712c4..cee442e56c 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexIntegerFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexIntegerFormatModel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,6 +31,6 @@ public class HexIntegerFormatModel extends HexValueFormatModel { throws ByteBlockAccessException { int i = block.getInt(index); String str = Integer.toHexString(i); - return pad(str); + return DataFormatModel.pad(str, symbolSize); } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongFormatModel.java index f16327cebe..74f8c57cb4 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongFormatModel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,6 +31,6 @@ public class HexLongFormatModel extends HexValueFormatModel { throws ByteBlockAccessException { long l = block.getLong(index); String str = Long.toHexString(l); - return pad(str); + return DataFormatModel.pad(str, symbolSize); } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongLongFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongLongFormatModel.java index ec64c018d5..b9032fd272 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongLongFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexLongLongFormatModel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,6 +17,8 @@ package ghidra.app.plugin.core.format; import java.math.BigInteger; +import ghidra.util.DataConverter; + /** * Converts byte values to LongLong represented as an 16-byte/32-digit hex number. */ @@ -29,11 +31,13 @@ public class HexLongLongFormatModel extends HexValueFormatModel { @Override public String getDataRepresentation(ByteBlock block, BigInteger index) throws ByteBlockAccessException { - long l0 = block.getLong(index); - String str0 = pad(Long.toHexString(l0)); - long l1 = block.getLong(index.add(BigInteger.valueOf(8))); - String str1 = pad(Long.toHexString(l1)); - String str = str1.substring(16) + str0.substring(16); - return pad(str); + byte[] bytes = new byte[nbytes]; + if (block.getBytes(bytes, index, nbytes) != nbytes) { + return fullSymbolErrorStr; + } + + DataConverter dc = DataConverter.getInstance(block.isBigEndian()); + BigInteger val = dc.getBigInteger(bytes, nbytes, false); + return "%032x".formatted(val); } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexShortFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexShortFormatModel.java index 2b2f937e20..7ca5cd1580 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexShortFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexShortFormatModel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -30,7 +30,7 @@ public class HexShortFormatModel extends HexValueFormatModel { public String getDataRepresentation(ByteBlock block, BigInteger index) throws ByteBlockAccessException { short s = block.getShort(index); - String str = Integer.toHexString(s & 0xFFFF); - return pad(str); + String str = Integer.toHexString(Short.toUnsignedInt(s)); + return DataFormatModel.pad(str, symbolSize); } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexValueFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexValueFormatModel.java index b6e9428348..a738429804 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexValueFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/HexValueFormatModel.java @@ -20,19 +20,21 @@ import java.math.BigInteger; import ghidra.util.HelpLocation; /** - * Converts byte values to value represented as a 2, 4, 8, or 16-byte hex number. + * Common base class for 2, 4, 8, or 16-byte hex number formats. */ -public abstract class HexValueFormatModel implements UniversalDataFormatModel { +public abstract class HexValueFormatModel + implements UniversalDataFormatModel, MutableDataFormatModel { - protected String name; - - private int symbolSize; - protected int nbytes; + protected final String name; + protected final int symbolSize; + protected final int nbytes; + protected final String fullSymbolErrorStr; public HexValueFormatModel(String name, int nbytes) { this.name = name; this.nbytes = nbytes; - symbolSize = nbytes * 2; + this.symbolSize = nbytes * 2; + this.fullSymbolErrorStr = "??".repeat(nbytes); } @Override @@ -76,11 +78,6 @@ public abstract class HexValueFormatModel implements UniversalDataFormatModel { public abstract String getDataRepresentation(ByteBlock block, BigInteger index) throws ByteBlockAccessException; - @Override - public boolean isEditable() { - return true; - } - @Override public boolean replaceValue(ByteBlock block, BigInteger index, int charPosition, char c) throws ByteBlockAccessException { @@ -100,44 +97,11 @@ public abstract class HexValueFormatModel implements UniversalDataFormatModel { return true; } - @Override - public int getGroupSize() { - return 1; - } - - /** - * Set the number of units in a group. This format does not - * support groups. - */ - @Override - public void setGroupSize(int groupSize) { - throw new UnsupportedOperationException("groups are not supported"); - } - @Override public int getUnitDelimiterSize() { return 1; } - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return bytesPerLine % nbytes == 0; - } - - /** - * Returns value with leading zeros. - */ - protected String pad(String value) { - StringBuffer sb = new StringBuffer(); - int len = symbolSize - value.length(); - - for (int i = 0; i < len; i++) { - sb.append("0"); - } - sb.append(value); - return sb.toString(); - } - /** * adjust byte b to use either the upper 4 bits or * the lower 4 bits of newb according to charPosition. @@ -157,12 +121,6 @@ public abstract class HexValueFormatModel implements UniversalDataFormatModel { @Override public HelpLocation getHelpLocation() { - String anchor = name.replaceAll("\\s", ""); - return new HelpLocation("ByteViewerPlugin", anchor); + return new HelpLocation("ByteViewerPlugin", name.replaceAll(" ", "")); // "Hex Long" -> "HexLong" } - - @Override - public void dispose() { - } - } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/IntegerFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/IntegerFormatModel.java index 1c0ac4962d..d5c18127b0 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/IntegerFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/IntegerFormatModel.java @@ -15,10 +15,10 @@ */ package ghidra.app.plugin.core.format; -import ghidra.util.HelpLocation; - import java.math.BigInteger; +import ghidra.util.HelpLocation; + /** * Converts byte values to Integer representation in decimal format. * This formatter does not allow editing. @@ -28,8 +28,7 @@ public class IntegerFormatModel implements UniversalDataFormatModel { private int symbolSize; public IntegerFormatModel() { - - symbolSize = 11; // 1 char for sign + this.symbolSize = 11; // 1 char for sign } /** @@ -93,59 +92,8 @@ public class IntegerFormatModel implements UniversalDataFormatModel { // determine what bytes to get int i = block.getInt(index); - String str = Integer.toString(i); + return DataFormatModel.pad(Integer.toString(i), symbolSize, " "); - return pad(str); - - } - - /** - * Returns false to allow no values to be changed. - */ - @Override - public boolean isEditable() { - return false; - } - - /** - * Overwrite a value in a ByteBlock. - * @param block block to change - * @param index byte index into the block - * @param pos The position within the unit where c will be the - * new character. - * @param c new character to put at pos param - * @return true if the replacement is legal, false if the - * replacement value would not make sense for this format, e.g. - * attempt to put a 'z' in a hex unit. - * @throws ByteBlockAccessException if the block cannot be updated - * @throws IndexOutOfBoundsException if index is not valid for the - * block - */ - @Override - public boolean replaceValue(ByteBlock block, BigInteger index, int pos, char c) - throws ByteBlockAccessException { - - return false; - } - - /** - * Get number of units in a group. A group may represent - * multiple units shown as one entity. This format does not - * support groups. - */ - @Override - public int getGroupSize() { - return 1; - } - - /** - * Set the number of units in a group. This format does not - * support groups. - * @throws UnsupportedOperationException - */ - @Override - public void setGroupSize(int groupSize) { - throw new UnsupportedOperationException("groups are not supported"); } /** @@ -156,46 +104,8 @@ public class IntegerFormatModel implements UniversalDataFormatModel { return 1; } - /** - * @see ghidra.app.plugin.core.format.DataFormatModel#validateBytesPerLine(int) - */ - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return bytesPerLine % 4 == 0; - } - - ///////////////////////////////////////////////////////////////// - // *** private methods *** - ///////////////////////////////////////////////////////////////// - - /** - * Returns value with leading zeros if the value - * represents a positive number; returns value - * with leading blanks if the value represents a - * negative number. - */ - private String pad(String value) { - StringBuffer sb = new StringBuffer(); - int len = symbolSize - value.length(); - - for (int i = 0; i < len; i++) { - sb.append(" "); - } - sb.append(value); - return (sb.toString()); - } - - /* (non-Javadoc) - * @see ghidra.app.plugin.format.DataFormatModel#getHelpLocation() - */ @Override public HelpLocation getHelpLocation() { return new HelpLocation("ByteViewerPlugin", "Integer"); } - - @Override - public void dispose() { - // nothing to do - } - } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/MutableDataFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/MutableDataFormatModel.java new file mode 100644 index 0000000000..a274b4cc69 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/MutableDataFormatModel.java @@ -0,0 +1,42 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.format; + +import java.math.BigInteger; + +/** + * Optional interface for DataFormatModels that allows the user to modify byte values using + * the model's representation format. + */ +public interface MutableDataFormatModel extends DataFormatModel { + /** + * Overwrite a value in a ByteBlock. + * @param block block to change + * @param index byte index into the block + * @param pos The position within the unit where c will be the + * new character. + * @param c new character to put at pos param + * @return true if the replacement is legal, false if the + * replacement value would not make sense for this format, e.g. + * attempt to put a 'z' in a hex unit. + * @throws ByteBlockAccessException if the block cannot be updated + * @throws IndexOutOfBoundsException if index is not valid for the + * block + */ + boolean replaceValue(ByteBlock block, BigInteger index, int pos, char c) + throws ByteBlockAccessException; + +} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/OctalFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/OctalFormatModel.java index 993d9204a6..1f3aebe4f9 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/OctalFormatModel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/OctalFormatModel.java @@ -15,14 +15,14 @@ */ package ghidra.app.plugin.core.format; -import ghidra.util.HelpLocation; - import java.math.BigInteger; +import ghidra.util.HelpLocation; + /** * Converts byte values to Octal representation. */ -public class OctalFormatModel implements UniversalDataFormatModel { +public class OctalFormatModel implements UniversalDataFormatModel, MutableDataFormatModel { private int symbolSize; private static final String GOOD_CHARS = "01234567"; @@ -90,32 +90,18 @@ public class OctalFormatModel implements UniversalDataFormatModel { throws ByteBlockAccessException { byte b = block.getByte(index); - int i = b; - i &= 0xff; // 0377 + int i = Byte.toUnsignedInt(b); - String str = Integer.toOctalString(i); + String str = Integer.toOctalString(i); // "377" is max - if (str.length() > symbolSize) { - str = str.substring(str.length() - symbolSize); - } - - return pad(str); - - } - - /** - * Returns true to allow values to be changed. - */ - @Override - public boolean isEditable() { - return (true); + return DataFormatModel.pad(str, symbolSize); } /** * Overwrite a value in a ByteBlock. * @param block block to change * @param index byte index into the block - * @param pos The position within the unit where c will be the + * @param charPosition The position within the unit where c will be the * new character. * @param c new character to put at pos param * @return true if the replacement is legal, false if the @@ -164,26 +150,6 @@ public class OctalFormatModel implements UniversalDataFormatModel { return true; } - /** - * Get number of units in a group. A group may represent - * multiple units shown as one entity. This format does not - * support groups. - */ - @Override - public int getGroupSize() { - return 1; - } - - /** - * Set the number of units in a group. This format does not - * support groups. - * @throws UnsupportedOperationException - */ - @Override - public void setGroupSize(int groupSize) { - throw new UnsupportedOperationException("groups are not supported"); - } - /** * Get the number of characters separating units. */ @@ -192,38 +158,8 @@ public class OctalFormatModel implements UniversalDataFormatModel { return 1; } - /** - * @see ghidra.app.plugin.core.format.DataFormatModel#validateBytesPerLine(int) - */ - @Override - public boolean validateBytesPerLine(int bytesPerLine) { - return true; - } - - ///////////////////////////////////////////////////////////////// - // *** private methods *** - ///////////////////////////////////////////////////////////////// - private String pad(String value) { - StringBuffer sb = new StringBuffer(); - int len = symbolSize - value.length(); - - for (int i = 0; i < len; i++) { - sb.append("0"); - } - sb.append(value); - return (sb.toString()); - } - - /* (non-Javadoc) - * @see ghidra.app.plugin.format.DataFormatModel#getHelpLocation() - */ @Override public HelpLocation getHelpLocation() { return new HelpLocation("ByteViewerPlugin", "Octal"); } - - @Override - public void dispose() { - // nothing to do - } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/TooltipDataFormatModel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/TooltipDataFormatModel.java new file mode 100644 index 0000000000..27fcef2e2d --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/format/TooltipDataFormatModel.java @@ -0,0 +1,27 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.format; + +import java.math.BigInteger; + +import ghidra.app.plugin.core.byteviewer.ByteViewerComponent; + +/** + * Optional interface for {@link DataFormatModel}s that want to provide tooltip popups. + */ +public interface TooltipDataFormatModel extends DataFormatModel { + String getTooltip(ByteBlock block, BigInteger index, ByteViewerComponent comp); +} diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/AbstractByteViewerPluginTest.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/AbstractByteViewerPluginTest.java new file mode 100644 index 0000000000..918f4dd643 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/AbstractByteViewerPluginTest.java @@ -0,0 +1,516 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.byteviewer; + +import static org.junit.Assert.*; + +import java.awt.*; +import java.awt.event.KeyEvent; +import java.util.*; +import java.util.List; + +import javax.swing.JLabel; + +import org.junit.After; +import org.junit.Before; + +import docking.*; +import docking.action.*; +import docking.menu.ToolBarItemManager; +import docking.menu.ToolBarManager; +import docking.widgets.fieldpanel.FieldPanel; +import docking.widgets.fieldpanel.field.Field; +import docking.widgets.fieldpanel.field.SimpleTextField; +import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.plugin.core.format.*; +import ghidra.app.services.GoToService; +import ghidra.app.services.ProgramManager; +import ghidra.app.util.viewer.field.OperandFieldFactory; +import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.Memory; +import ghidra.program.model.mem.MemoryAccessException; +import ghidra.program.util.ProgramLocation; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.TestEnv; + +public abstract class AbstractByteViewerPluginTest extends AbstractGhidraHeadedIntegrationTest { + + protected TestEnv env; + protected PluginTool tool; + protected Program program; + protected ByteViewerPlugin plugin; + protected ByteViewerPanel panel; + protected CodeBrowserPlugin cbPlugin; + protected ProgramByteViewerComponentProvider provider; + protected Memory memory; + protected Listing listing; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + tool = env.showTool(); + for (Class pluginClass : getDefaultPlugins()) { + tool.addPlugin(pluginClass.getName()); + } + tool.addPlugin(ByteViewerPlugin.class.getName()); + + plugin = env.getPlugin(ByteViewerPlugin.class); + provider = plugin.getProvider(); + panel = provider.getByteViewerPanel(); + + tool.showComponentProvider(provider, true); + cbPlugin = env.getPlugin(CodeBrowserPlugin.class); + program = buildProgram(); + memory = program.getMemory(); + listing = program.getListing(); + ProgramManager pm = tool.getService(ProgramManager.class); + pm.openProgram(program.getDomainFile()); + } + + @After + public void tearDown() throws Exception { + env.dispose(); + } + + abstract protected List> getDefaultPlugins(); + + abstract protected Program buildProgram() throws Exception; + + protected void assertPosition(ByteViewerComponent view, String expectedFieldText, + int expectedColumn) { + + String fieldText = runSwing(() -> { + Field field = view.getCurrentField(); + String text = field.getText(); + return text; + }); + assertEquals(expectedFieldText, fieldText); + + FieldLocation location = runSwing(() -> view.getCursorLocation()); + assertEquals(expectedColumn, location.getCol()); + } + + protected void setEditMode(boolean b) { + ToggleDockingAction action = provider.getEditModeAction(); + if (action.isSelected() != b) { + performAction(action); + assertTrue(action.isSelected() == b); + } + } + + protected void gotoInPanel(Address addr) { + runSwing(() -> { + ProgramByteBlockSet blockset = (ProgramByteBlockSet) provider.getByteBlockSet(); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); + panel.setCursorLocation(bbInfo.getBlock(), bbInfo.getOffset(), bbInfo.getColumn()); + }); + } + + protected void goTo(Address addr) { + GoToService goToService = tool.getService(GoToService.class); + goToService.goTo(new ProgramLocation(program, addr)); + } + + protected void goToByte(String addr) { + goToByte(addr(addr)); + } + + protected void goToByte(Address addr) { + ByteViewerComponent view = runSwing(() -> panel.getCurrentComponent()); + goToByte(view, addr); + } + + protected void goToByte(ByteViewerComponent view, Address addr) { + FieldLocation loc = getFieldLocation(addr); + runSwing(() -> { + view.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); + }); + } + + protected void setByteViewerLocation(Address address) throws Exception { + setByteViewerLocation(provider, address); + } + + protected void setByteViewerLocation(ProgramByteViewerComponentProvider testProvider, + Address address) throws Exception { + ByteViewerPanel byteViewerPanel = testProvider.getByteViewerPanel(); + ByteViewerComponent component = byteViewerPanel.getCurrentComponent(); + FieldLocation byteViewerLocation = getFieldLocation(address); + + runSwing(() -> { + component.setCursorPosition(byteViewerLocation.getIndex(), + byteViewerLocation.getFieldNum(), byteViewerLocation.getRow(), + byteViewerLocation.getCol()); + component.scrollToCursor(); + }); + } + + protected void setViewSelected(ByteViewerOptionsDialog dialog, String viewName, + boolean selected) { + runSwing(() -> dialog.setModelSelected(viewName, selected)); + } + + protected ByteViewerOptionsDialog launchByteViewerOptions() { + DockingAction action = provider.getOptionsAction(); + assertTrue(action.isEnabled()); + + runSwing(() -> action.actionPerformed(new DefaultActionContext()), false); + waitForSwing(); + ByteViewerOptionsDialog d = waitForDialogComponent(ByteViewerOptionsDialog.class); + return d; + } + + protected ByteViewerComponent setView(String name) { + ByteViewerComponent view = getView(name); + setView(view); + return view; + } + + protected void setView(ByteViewerComponent view) { + runSwing(() -> panel.setCurrentView(view)); + } + + protected ByteViewerComponent getView(String name) { + ByteViewerComponent c = runSwing(() -> panel.getComponentByName(name)); + if (c == null) { + fail("Cannot find view '" + name + "'"); + } + return c; + } + + protected ByteViewerComponent getCurrentView() { + return panel.getCurrentComponent(); + } + + protected ByteField getField(String viewName, Address addr) { + return runSwing(() -> { + ByteViewerComponent c = panel.getComponentByName(viewName); + ProgramByteBlockSet blockset = (ProgramByteBlockSet) provider.getByteBlockSet(); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); + FieldLocation loc = c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); + return c.getField(loc.getIndex(), loc.getFieldNum()); + }); + } + + protected ByteField getField(ByteViewerComponent c) { + return runSwing(() -> { + FieldLocation loc = c.getCursorLocation(); + return c.getField(loc.getIndex(), loc.getFieldNum()); + }); + } + + protected ByteField getField(ByteViewerComponent c, Address addr) { + return runSwing(() -> { + ProgramByteBlockSet blockset = (ProgramByteBlockSet) provider.getByteBlockSet(); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); + FieldLocation loc = c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); + return c.getField(loc.getIndex(), loc.getFieldNum()); + }); + } + + protected FieldLocation getFieldLocation(Address addr) { + return runSwing(() -> { + ByteViewerComponent c = panel.getCurrentComponent(); + ProgramByteBlockSet blockset = (ProgramByteBlockSet) provider.getByteBlockSet(); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); + return c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); + }); + } + + protected FieldLocation getFieldLocation(ByteViewerComponent c, Address addr) { + return runSwing(() -> { + ProgramByteBlockSet blockset = (ProgramByteBlockSet) provider.getByteBlockSet(); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); + return c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); + }); + } + + protected Address addr(long offset) { + return program.getImageBase().getNewAddress(offset); + } + + protected Address addr(String offset) { + return program.getAddressFactory().getAddress(offset); + } + + protected Address convertToAddr(ByteBlockInfo info) { + return ((ProgramByteBlockSet) provider.getByteBlockSet()).getAddress(info.getBlock(), + info.getOffset()); + } + + protected boolean byteBlockSelectionEquals(ByteBlockSelection b1, ByteBlockSelection b2) { + + int nRanges = b1.getNumberOfRanges(); + if (nRanges != b2.getNumberOfRanges()) { + return false; + } + for (int i = 0; i < nRanges; i++) { + ByteBlockRange range1 = b1.getRange(i); + ByteBlockRange range2 = b2.getRange(i); + if (!range1.equals(range2)) { + return false; + } + } + return true; + } + + protected ByteViewerComponent findComponent(Container container, String name) { + Component[] c = container.getComponents(); + for (Component element : c) { + if (element instanceof ByteViewerComponent) { + if (((ByteViewerComponent) element).getDataModel().getName().equals(name)) { + return (ByteViewerComponent) element; + } + } + else if (element instanceof Container) { + ByteViewerComponent bvc = findComponent((Container) element, name); + if (bvc != null) { + return bvc; + } + } + } + return null; + } + + protected void loadViews(String... viewNames) { + enableViews(true, viewNames); + } + + protected void enableViews(boolean b, String... viewNames) { + assertNotEquals(0, viewNames.length); + ByteViewerOptionsDialog dialog = launchByteViewerOptions(); + for (String name : viewNames) { + setViewSelected(dialog, name, b); + } + pressButtonByText(dialog.getComponent(), "OK"); + } + + protected Container findContainer(Container parent, Class theClass) { + Component[] c = parent.getComponents(); + for (Component element : c) { + if (element.getClass() == theClass) { + return (Container) element; + } + if (element instanceof Container) { + Container container = findContainer((Container) element, theClass); + if (container != null) { + return container; + } + } + } + return null; + } + + protected String findLabelStr(Container container, String name) { + Component[] c = container.getComponents(); + for (Component element : c) { + if (element instanceof JLabel) { + if (name.equals(element.getName())) { + return ((JLabel) element).getText(); + } + } + if (element instanceof Container) { + String str = findLabelStr((Container) element, name); + if (str != null) { + return str; + } + } + } + return null; + } + + protected void setViewWidth(String name, int width) { + runSwing(() -> { + panel.setViewWidth(name, width); + }); + } + + protected int getViewWidth(String name) { + return runSwing(() -> { + return panel.getViewWidth(name); + }); + } + + protected void goTo(Address a, String fieldName) { + int row = 0; + int col = 0; + assertTrue(cbPlugin.goToField(a, fieldName, row, col)); + } + + protected void leftArrow() { + runSwing(() -> { + ByteViewerComponent view = panel.getCurrentComponent(); + view.cursorLeft(); + }); + } + + protected void rightArrow() { + + runSwing(() -> { + ByteViewerComponent view = panel.getCurrentComponent(); + view.cursorRight(); + }); + } + + protected void pressKey(int modifiers, int keyCode, char keyChar) { + runSwing(() -> { + ByteViewerComponent c = panel.getCurrentComponent(); + KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), modifiers, keyCode, keyChar); + FieldLocation loc = c.getCursorLocation(); + c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), + c.getCurrentField()); + }); + } + + protected void assertByteData(Address addr, int byteValue) throws MemoryAccessException { + byte b = memory.getByte(addr); + assertEquals(byteValue, Byte.toUnsignedInt(b)); + } + + protected void assertOffsetInfo(int offset, String addr) { + assertEquals(offset, runSwing(() -> provider.getConfigOptions().getOffset()).intValue()); + assertEquals(Integer.toString(offset), findLabelStr(provider.getComponent(), "Offset")); + assertEquals(addr, findLabelStr(provider.getComponent(), "Insertion")); + } + + protected void assertFieldLocationInfo(String addr, int index, int fieldNum) { + assertFieldLocationInfo(addr(addr), index, fieldNum); + } + + protected void assertFieldLocationInfo(Address addr, int index, int fieldNum) { + FieldLocation floc = getFieldLocation(addr); + assertEquals(index, floc.getIndex().intValue()); + assertEquals(fieldNum, floc.getFieldNum()); + } + + @SuppressWarnings("unchecked") + protected void assertOnlyOneProviderToolbarAction() { + + DockingWindowManager dwm = tool.getWindowManager(); + ActionToGuiMapper guiActions = + (ActionToGuiMapper) getInstanceField("actionToGuiMapper", dwm); + GlobalMenuAndToolBarManager menuManager = + (GlobalMenuAndToolBarManager) getInstanceField("menuAndToolBarManager", guiActions); + + Map windowToActionManagerMap = + (Map) getInstanceField("windowToActionManagerMap", + menuManager); + + DockingActionIf showAction = + (DockingActionIf) getInstanceField("showProviderAction", provider); + String actionName = showAction.getName(); + List matches = new ArrayList<>(); + for (WindowActionManager actionManager : windowToActionManagerMap.values()) { + + ToolBarManager toolbarManager = + (ToolBarManager) getInstanceField("toolBarMgr", actionManager); + Map> groupToItems = + (Map>) getInstanceField("groupToItemsMap", + toolbarManager); + + Collection> values = groupToItems.values(); + for (List list : values) { + for (ToolBarItemManager manager : list) { + DockingActionIf action = manager.getAction(); + if (actionName.equals(action.getName())) { + matches.add(action); + } + } + } + } + + assertEquals("Should only have 1 action on toolbar to show the provider", 1, + matches.size()); + } + + protected void goToOperand(String addr) { + goTo(addr(addr), OperandFieldFactory.FIELD_NAME); + } + + protected void rightArrowListing() { + FieldPanel fp = cbPlugin.getFieldPanel(); + runSwing(() -> fp.cursorRight()); + } + + protected void leftArrowListing() { + FieldPanel fp = cbPlugin.getFieldPanel(); + runSwing(() -> fp.cursorLeft()); + } + + protected void assertListingPosition(String expectedFieldText, int expectedColumn) { + FieldPanel fp = cbPlugin.getFieldPanel(); + + String fieldText = runSwing(() -> { + Field field = fp.getCurrentField(); + String text = field.getText(); + return text; + }); + assertEquals(expectedFieldText, fieldText); + + FieldLocation location = runSwing(() -> fp.getCursorLocation()); + assertEquals(expectedColumn, location.getCol()); + } + + protected void assertFieldColor(FieldLocation loc, Color expectedColor) { + assertFieldColor(loc, expectedColor, false); + } + + protected void assertFieldColor(FieldLocation loc, Color expectedColor, boolean allowNull) { + ByteViewerComponent c = panel.getCurrentComponent(); + ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); + assertFieldColor(field, expectedColor, allowNull); + } + + protected void assertFieldColor(SimpleTextField field, Color expectedColor) { + assertFieldColor(field, expectedColor, false); + } + + protected void assertFieldColor(SimpleTextField field, Color expectedColor, boolean allowNull) { + Color c = field.getForeground(); + if (c == null && expectedColor != null) { + if (allowNull) { + return; + } + fail("Field had no color: " + field); + } + assertEquals(expectedColor, c); + } + + protected void assertColor(Color expectedColor, Color actualColor) { + assertColor(expectedColor, actualColor, false); + } + + protected void assertColor(Color expectedColor, Color actualColor, boolean allowNull) { + if (actualColor == null && expectedColor != null) { + if (allowNull) { + return; + } + fail("Color was null, expected " + expectedColor); + } + assertEquals(expectedColor, actualColor); + } + + protected void assertCursorColor(ByteViewerComponent c, Color expectedColor) { + assertColor(expectedColor, c.getFocusedCursorColor()); + + } + +} diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java index 6361c1058d..9fe33ad6d3 100644 --- a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java @@ -17,26 +17,19 @@ package ghidra.app.plugin.core.byteviewer; import static org.junit.Assert.*; -import java.awt.*; -import java.math.BigInteger; +import java.awt.Point; +import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.List; -import javax.swing.JCheckBox; -import javax.swing.JLabel; import javax.swing.table.JTableHeader; import javax.swing.table.TableColumnModel; -import org.junit.*; +import org.junit.Test; -import docking.*; +import docking.DefaultActionContext; +import docking.action.DockingAction; import docking.action.DockingActionIf; -import docking.menu.ToolBarItemManager; -import docking.menu.ToolBarManager; import docking.widgets.EventTrigger; -import docking.widgets.fieldpanel.FieldPanel; -import docking.widgets.fieldpanel.field.Field; -import docking.widgets.fieldpanel.support.FieldLocation; import docking.widgets.fieldpanel.support.FieldSelection; import ghidra.app.events.ProgramSelectionPluginEvent; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; @@ -44,11 +37,7 @@ import ghidra.app.plugin.core.format.*; import ghidra.app.plugin.core.navigation.*; import ghidra.app.services.GoToService; import ghidra.app.services.ProgramManager; -import ghidra.app.util.AddressInput; -import ghidra.app.util.bean.FixedBitSizeValueField; -import ghidra.app.util.viewer.field.OperandFieldFactory; import ghidra.framework.plugintool.Plugin; -import ghidra.framework.plugintool.PluginTool; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSet; @@ -56,41 +45,21 @@ import ghidra.program.model.data.StringDataType; import ghidra.program.model.listing.CodeUnit; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramSelection; -import ghidra.test.AbstractGhidraHeadedIntegrationTest; -import ghidra.test.TestEnv; /** * Basic tests for the byte view plugin */ -public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { +public class ByteViewerPlugin1Test extends AbstractByteViewerPluginTest { - private TestEnv env; - private PluginTool tool; - private Program program; - private ByteViewerPlugin plugin; - private ByteViewerPanel panel; - private CodeBrowserPlugin cbPlugin; - @Before - public void setUp() throws Exception { - env = new TestEnv(); - tool = env.showTool(); - tool.addPlugin(NavigationHistoryPlugin.class.getName()); - tool.addPlugin(NextPrevAddressPlugin.class.getName()); - tool.addPlugin(CodeBrowserPlugin.class.getName()); - tool.addPlugin(GoToAddressLabelPlugin.class.getName()); - tool.addPlugin(ByteViewerPlugin.class.getName()); - - plugin = env.getPlugin(ByteViewerPlugin.class); - tool.showComponentProvider(plugin.getProvider(), true); - cbPlugin = env.getPlugin(CodeBrowserPlugin.class); - program = buildNotepad(); - ProgramManager pm = tool.getService(ProgramManager.class); - pm.openProgram(program.getDomainFile()); - panel = plugin.getProvider().getByteViewerPanel(); + @Override + protected List> getDefaultPlugins() { + return List.of(NavigationHistoryPlugin.class, NextPrevAddressPlugin.class, + CodeBrowserPlugin.class, GoToAddressLabelPlugin.class); } - private Program buildNotepad() throws Exception { + @Override + protected Program buildProgram() throws Exception { ProgramBuilder builder = new ProgramBuilder("notepad", ProgramBuilder._TOY); builder.createMemory("test2", "0x1001000", 1000); builder.createMemory("mem1", "0xf0001300", 1000); @@ -99,29 +68,18 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { builder.setBytes("0x1001000", "00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff"); // ascii - "message" - builder.setBytes("0x1001100", "6d 65 73 73 61 67 65 00"); + builder.setBytes("0x1001100", "message\0".getBytes(StandardCharsets.US_ASCII)); builder.applyDataType("0x1001100", new StringDataType()); return builder.getProgram(); } - private Program build8051() throws Exception { - ProgramBuilder notepadBuilder = new ProgramBuilder("seg", ProgramBuilder._X86_16_REAL_MODE); - notepadBuilder.createMemory("mem1", "0000:0000", 0x8000); - return notepadBuilder.getProgram(); - } - - @After - public void tearDown() throws Exception { - env.dispose(); - } - @Test public void testOpenProgram() { assertEquals("Hex", panel.getCurrentComponent().getDataModel().getName()); - DockingActionIf action = getAction(plugin, "Byte Viewer Options"); + DockingActionIf action = provider.getOptionsAction(); assertTrue(action.isEnabled()); - assertEquals(1, panel.getNumberOfViews()); + assertEquals(1, panel.getViewList().size()); } @Test @@ -149,7 +107,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { }); waitForSwing(); - program = buildNotepad(); + program = buildProgram(); runSwing(() -> { ProgramManager pm = tool.getService(ProgramManager.class); pm.openProgram(program.getDomainFile()); @@ -161,19 +119,16 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testAddRemoveViews() throws Exception { - ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - setViewSelected(dialog, "Ascii", true); - setViewSelected(dialog, "Octal", true); - pressButtonByText(dialog.getComponent(), "OK"); + loadViews("Chars", "Octal"); - assertEquals(3, panel.getNumberOfViews()); + assertEquals(3, panel.getViewList().size()); List names = panel.getViewNamesInDisplayOrder(); assertEquals(3, names.size()); Set viewNames = new HashSet<>(names); assertTrue(viewNames.contains("Hex")); assertTrue(viewNames.contains("Octal")); - assertTrue(viewNames.contains("Ascii")); + assertTrue(viewNames.contains("Chars")); // verify cursor position is the same across all views // verify the view is visible @@ -181,7 +136,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { assertTrue(bc.isVisible()); ByteBlockInfo bbInfo = bc.getViewerCursorLocation(); - bc = findComponent(panel, "Ascii"); + bc = findComponent(panel, "Chars"); assertTrue(bc.isVisible()); assertEquals(bbInfo, bc.getViewerCursorLocation()); @@ -189,27 +144,23 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { assertTrue(bc.isVisible()); assertEquals(bbInfo, bc.getViewerCursorLocation()); - dialog = launchByteViewerOptions(); - setViewSelected(dialog, "Ascii", false); - setViewSelected(dialog, "Hex", false); - pressButtonByText(dialog.getComponent(), "OK"); + enableViews(false, "Chars", "Hex"); - assertEquals(1, panel.getNumberOfViews()); + assertEquals(1, panel.getViewList().size()); names = panel.getViewNamesInDisplayOrder(); assertEquals(1, names.size()); assertEquals("Octal", names.get(0)); - } @Test public void testSetSelection() throws Exception { - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); final FieldSelection fsel = new FieldSelection(); fsel.addRange(0, 0, 8, 7); - ByteViewerComponent ascii = getView("Ascii"); + ByteViewerComponent ascii = getView("Chars"); setView(ascii); runSwing(() -> { ascii.selectionChanged(fsel, EventTrigger.GUI_ACTION); @@ -225,7 +176,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { // convert bsel to an address set AddressSet set = - ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()).getAddressSet(bsel); + ((ProgramByteBlockSet) provider.getByteBlockSet()).getAddressSet(bsel); // subtract one address from code browser selection since it is on // a code unit boundary @@ -236,7 +187,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testSetSelectionWithMouse() throws Exception { - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); ByteViewerComponent c = panel.getCurrentComponent(); GoToService goToService = tool.getService(GoToService.class); @@ -244,7 +195,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { Point startPoint = c.getCursorPoint(); - goToByte("0x010010bc"); + goTo(addr(0x010010bc)); Point endPoint = c.getCursorPoint(); dragMouse(c, 1, startPoint.x, startPoint.y, endPoint.x, endPoint.y, 0); @@ -258,14 +209,13 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { ByteBlockSelection bsel = panel.getViewerSelection(); // convert bsel to an address set - AddressSet set = - ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()).getAddressSet(bsel); + AddressSet set = ((ProgramByteBlockSet) provider.getByteBlockSet()).getAddressSet(bsel); assertTrue(psel.hasSameAddresses(set)); } @Test public void testProcessSelectionEvent() throws Exception { - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); ByteViewerComponent c = panel.getCurrentComponent(); @@ -281,7 +231,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { c = findComponent(panel, "Octal"); assertTrue(byteBlockSelectionEquals(bsel, c.getViewerSelection())); - c = findComponent(panel, "Ascii"); + c = findComponent(panel, "Chars"); assertTrue(byteBlockSelectionEquals(bsel, c.getViewerSelection())); ProgramSelection psel = cbPlugin.getCurrentSelection(); @@ -289,7 +239,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { bsel = panel.getViewerSelection(); // convert bsel to an address set - set = ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()).getAddressSet(bsel); + set = ((ProgramByteBlockSet) provider.getByteBlockSet()).getAddressSet(bsel); assertTrue(psel.hasSameAddresses(set)); } @@ -304,19 +254,19 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { }); ByteViewerComponent c = panel.getCurrentComponent(); ByteBlockSelection sel = c.getViewerSelection(); - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); // verify that the selections are the same after adding views ByteViewerComponent octalC = findComponent(panel, "Octal"); assertTrue(byteBlockSelectionEquals(sel, octalC.getViewerSelection())); - ByteViewerComponent asciiC = findComponent(panel, "Ascii"); + ByteViewerComponent asciiC = findComponent(panel, "Chars"); assertTrue(byteBlockSelectionEquals(sel, asciiC.getViewerSelection())); } @Test public void testMultiRangeSelection() throws Exception { - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); final FieldSelection fsel = new FieldSelection(); fsel.addRange(0, 0, 4, 7); @@ -332,21 +282,21 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { ByteViewerComponent octalC = findComponent(panel, "Octal"); assertTrue(byteBlockSelectionEquals(sel, octalC.getViewerSelection())); - ByteViewerComponent asciiC = findComponent(panel, "Ascii"); + ByteViewerComponent asciiC = findComponent(panel, "Chars"); assertTrue(byteBlockSelectionEquals(sel, asciiC.getViewerSelection())); } @Test public void testLocationChanges() throws Exception { - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); ByteViewerComponent c = panel.getCurrentComponent(); - goToByte("0x01001004"); + goTo(addr(0x01001004)); ByteBlockInfo info = c.getViewerCursorLocation(); - ByteViewerComponent ascii = getView("Ascii"); + ByteViewerComponent ascii = getView("Chars"); assertEquals(info, ascii.getViewerCursorLocation()); ByteViewerComponent octal = getView("Octal"); assertEquals(info, octal.getViewerCursorLocation()); @@ -357,7 +307,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { assertEquals(addr, cbPlugin.getCurrentAddress()); - goToByte("0x01001081"); + goTo(addr(0x01001081)); info = c.getViewerCursorLocation(); assertEquals(info, ascii.getViewerCursorLocation()); @@ -370,7 +320,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { CodeUnit cu = program.getListing().getCodeUnitContaining(addr); assertEquals(cu.getMinAddress(), cbPlugin.getCurrentAddress()); - goToByte("0xf0001300"); + goTo(addr(0xf0001300)); info = c.getViewerCursorLocation(); assertEquals(info, ascii.getViewerCursorLocation()); @@ -428,7 +378,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { ByteViewerComponent octal = getView("Octal"); setView(hex); - goToByte("0x01001004"); + goTo(addr(0x01001004)); assertPosition(hex, "44", 0); assertPosition(octal, "104", 0); @@ -451,7 +401,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { ByteViewerComponent octal = getView("Octal"); setView(octal); - goToByte("0x01001004"); + goTo(addr(0x01001004)); assertPosition(hex, "44", 0); assertPosition(octal, "104", 0); @@ -474,7 +424,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { ByteViewerComponent octal = getView("Octal"); setView(octal); - goToByte("0x01001005"); + goTo(addr(0x01001005)); assertPosition(hex, "55", 0); assertPosition(octal, "125", 0); @@ -527,70 +477,54 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { public void testBytesPerLinesChange() throws Exception { ByteViewerOptionsDialog d = launchByteViewerOptions(); - FixedBitSizeValueField field = - (FixedBitSizeValueField) getInstanceField("bytesPerLineField", d); - runSwing(() -> field.setValue(BigInteger.valueOf(10))); + runSwing(() -> d.setBytesPerLine(10)); pressButtonByText(d.getComponent(), "OK"); waitForSwing(); // verify that the bytes per line is 10 - assertEquals(10, plugin.getProvider().getBytesPerLine()); + ByteViewerConfigOptions configOptions = provider.getConfigOptions(); + assertEquals(10, configOptions.getBytesPerLine()); + ByteViewerComponent c = panel.getCurrentComponent(); - assertEquals(10, c.getNumberOfFields()); - assertEquals(8, plugin.getProvider().getOffset()); + assertEquals(10, runSwing(() -> c.getNumberOfFields()).intValue()); } @Test - public void testSetAlignedAddress() throws Exception { + public void testOffsetChange() { + goTo(addr(0x0100100b)); - goToByte("0x0100100b"); + int offsetOfAddr = 16 /* bytes_per_line*/ - (0x0100100b - 0x1001000); // == 5 ByteViewerOptionsDialog d = launchByteViewerOptions(); - AddressInput ai = (AddressInput) getInstanceField("addressInputField", d); - - // verify that the text field has the min address since the - // current offset is 0 - assertEquals(program.getMinAddress(), ai.getAddress()); - - runSwing(() -> ai.setText("0100100b")); + runSwing(() -> d.setOffset(offsetOfAddr)); pressButtonByText(d.getComponent(), "OK"); - // verify that offset label on the plugin shows '5' - assertEquals(5, plugin.getProvider().getOffset()); - assertEquals("5", findLabelStr(plugin.getProvider().getComponent(), "Offset")); - assertEquals("0100100b", findLabelStr(plugin.getProvider().getComponent(), "Insertion")); - FieldLocation floc = getFieldLocation(addr(0x100101b)); - assertEquals(2, floc.getIndex().intValue()); - assertEquals(0, floc.getFieldNum()); + // verify that offset label on the plugin shows the new offset + assertOffsetInfo(5, "0100100b"); - floc = getFieldLocation(addr(0x100102b)); - assertEquals(3, floc.getIndex().intValue()); - assertEquals(0, floc.getFieldNum()); + assertFieldLocationInfo(addr(0x0100100b), 1, 0 /* first field of the row */); + assertFieldLocationInfo(addr(0x0100102b), 3, 0 /* first field of the row */); } @Test - public void testSetBaseAddressSegmented() throws Exception { + public void testOffsetShiftViaToolbarButtonActions() { + DockingAction shiftLeftAction = provider.getShiftLeftAction(); + DockingAction shiftRightAction = provider.getShiftRightAction(); - ProgramManager pm = tool.getService(ProgramManager.class); - pm.closeProgram(); + goTo(addr(0x0100100b)); + assertFieldLocationInfo(addr(0x0100100b), 0, 0xb); - waitForSwing(); - - program = build8051(); - pm.openProgram(program.getDomainFile()); - - ByteViewerOptionsDialog d = launchByteViewerOptions(); - AddressInput ai = (AddressInput) getInstanceField("addressInputField", d); - - // verify that the text field has the min address since the - // current offset is 0 - assertEquals(program.getMinAddress(), ai.getAddress()); - - runSwing(() -> ai.setText("0000:0c06")); - pressButtonByText(d.getComponent(), "OK"); - - assertEquals(10, plugin.getProvider().getOffset()); + for (int i = 0; i < 0xb; i++) { + runSwing(() -> shiftLeftAction.actionPerformed(new DefaultActionContext())); + assertOffsetInfo(16 - i - 1, "0100100b"); + assertFieldLocationInfo(addr(0x0100100b), 1, 0xb - i - 1); + } + for (int i = 0xb - 1; i > 0; i--) { + runSwing(() -> shiftRightAction.actionPerformed(new DefaultActionContext())); + assertOffsetInfo(16 - i, "0100100b"); + assertFieldLocationInfo(addr(0x0100100b), 1, 0xb - i); + } } @Test @@ -598,30 +532,32 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { ByteViewerComponent c = panel.getCurrentComponent(); panel.setCurrentView(c); // force component to have focus + assertEquals(16, runSwing(() -> c.getNumberOfFields()).intValue()); - goToByte("0x0100100b"); + goTo(addr(0x0100100b)); ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - FixedBitSizeValueField field = - (FixedBitSizeValueField) getInstanceField("groupSizeField", dialog); - // verify that the text field has the min address since the - // current offset is 0 - - runSwing(() -> field.setValue(BigInteger.valueOf(2))); + runSwing(() -> dialog.setHexGroupSize(2)); pressButtonByText(dialog.getComponent(), "OK"); - assertEquals(2, c.getDataModel().getGroupSize()); + DataFormatModel dataModel = c.getDataModel(); + if (!(dataModel instanceof HexFormatModel hexModel)) { + fail(); + return; + } + assertEquals(2, runSwing(() -> hexModel.getHexGroupSize()).intValue()); + assertEquals(8, runSwing(() -> c.getNumberOfFields()).intValue()); } @Test public void testReorderViews() throws Exception { - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); List names = panel.getViewNamesInDisplayOrder(); assertEquals("Hex", names.get(0)); assertEquals("Octal", names.get(1)); - assertEquals("Ascii", names.get(2)); + assertEquals("Chars", names.get(2)); final JTableHeader columnHeader = (JTableHeader) findContainer(panel, JTableHeader.class); // move column 3 to 2 @@ -632,7 +568,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { names = panel.getViewNamesInDisplayOrder(); assertEquals("Hex", names.get(0)); - assertEquals("Ascii", names.get(1)); + assertEquals("Chars", names.get(1)); assertEquals("Octal", names.get(2)); // move column 2 to 1 @@ -641,7 +577,7 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { columnModel.moveColumn(2, 1); }); names = panel.getViewNamesInDisplayOrder(); - assertEquals("Ascii", names.get(0)); + assertEquals("Chars", names.get(0)); assertEquals("Hex", names.get(1)); assertEquals("Octal", names.get(2)); @@ -649,29 +585,29 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testResizeViews() { - loadViews("Ascii", "Octal"); - assertNotEquals(200, getViewWidth("Ascii")); + loadViews("Chars", "Octal"); + assertNotEquals(200, getViewWidth("Chars")); - setViewWidth("Ascii", 200); + setViewWidth("Chars", 200); - assertEquals(200, panel.getViewWidth("Ascii")); + assertEquals(200, panel.getViewWidth("Chars")); } @Test public void testResizeViewsSaveState() { - loadViews("Ascii", "Octal"); - assertNotEquals(200, getViewWidth("Ascii")); + loadViews("Chars", "Octal"); + assertNotEquals(200, getViewWidth("Chars")); - setViewWidth("Ascii", 200); + setViewWidth("Chars", 200); env.saveRestoreToolState(); - assertEquals(200, panel.getViewWidth("Ascii")); + assertEquals(200, panel.getViewWidth("Chars")); } @Test public void testReorderViewsSaveState() throws Exception { - loadViews("Ascii", "Octal"); + loadViews("Chars", "Octal"); final JTableHeader columnHeader = (JTableHeader) findContainer(panel, JTableHeader.class); // move column 1 to 0 @@ -699,274 +635,4 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { assertOnlyOneProviderToolbarAction(); } -//================================================================================================== -// Private Methods -//================================================================================================== - - @SuppressWarnings("unchecked") - private void assertOnlyOneProviderToolbarAction() { - - DockingWindowManager dwm = tool.getWindowManager(); - ActionToGuiMapper guiActions = - (ActionToGuiMapper) getInstanceField("actionToGuiMapper", dwm); - GlobalMenuAndToolBarManager menuManager = - (GlobalMenuAndToolBarManager) getInstanceField("menuAndToolBarManager", guiActions); - - Map windowToActionManagerMap = - (Map) getInstanceField("windowToActionManagerMap", - menuManager); - - ProgramByteViewerComponentProvider provider = plugin.getProvider(); - DockingActionIf showAction = - (DockingActionIf) getInstanceField("showProviderAction", provider); - String actionName = showAction.getName(); - List matches = new ArrayList<>(); - for (WindowActionManager actionManager : windowToActionManagerMap.values()) { - - ToolBarManager toolbarManager = - (ToolBarManager) getInstanceField("toolBarMgr", actionManager); - Map> groupToItems = - (Map>) getInstanceField("groupToItemsMap", - toolbarManager); - - Collection> values = groupToItems.values(); - for (List list : values) { - for (ToolBarItemManager manager : list) { - DockingActionIf action = manager.getAction(); - if (actionName.equals(action.getName())) { - matches.add(action); - } - } - } - } - - assertEquals("Should only have 1 action on toolbar to show the provider", 1, - matches.size()); - } - - private void goToOperand(String addr) { - goTo(addr(addr), OperandFieldFactory.FIELD_NAME); - } - - private void goTo(Address a, String fieldName) { - int row = 0; - int col = 0; - assertTrue(cbPlugin.goToField(a, fieldName, row, col)); - } - - private void leftArrow() { - runSwing(() -> { - ByteViewerComponent view = panel.getCurrentComponent(); - view.cursorLeft(); - }); - } - - private void rightArrow() { - - runSwing(() -> { - ByteViewerComponent view = panel.getCurrentComponent(); - view.cursorRight(); - }); - } - - private void rightArrowListing() { - FieldPanel fp = cbPlugin.getFieldPanel(); - runSwing(() -> fp.cursorRight()); - } - - private void leftArrowListing() { - FieldPanel fp = cbPlugin.getFieldPanel(); - runSwing(() -> fp.cursorLeft()); - } - - private void assertListingPosition(String expectedFieldText, int expectedColumn) { - FieldPanel fp = cbPlugin.getFieldPanel(); - - String fieldText = runSwing(() -> { - Field field = fp.getCurrentField(); - String text = field.getText(); - return text; - }); - assertEquals(expectedFieldText, fieldText); - - FieldLocation location = runSwing(() -> fp.getCursorLocation()); - assertEquals(expectedColumn, location.getCol()); - } - - private void assertPosition(ByteViewerComponent view, String expectedFieldText, - int expectedColumn) { - - String fieldText = runSwing(() -> { - Field field = view.getCurrentField(); - String text = field.getText(); - return text; - }); - assertEquals(expectedFieldText, fieldText); - - FieldLocation location = runSwing(() -> view.getCursorLocation()); - assertEquals(expectedColumn, location.getCol()); - } - - private void goToByte(String addr) { - goToByte(addr(addr)); - } - - private void goToByte(Address addr) { - - ByteViewerComponent view = runSwing(() -> panel.getCurrentComponent()); - goToByte(view, addr); - } - - private void goToByte(ByteViewerComponent view, Address addr) { - FieldLocation loc = getFieldLocation(addr); - runSwing(() -> { - view.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); - } - - private void setViewSelected(ByteViewerOptionsDialog dialog, String viewName, - boolean selected) { - Map checkboxMap = (Map) getInstanceField("checkboxMap", dialog); - JCheckBox checkbox = (JCheckBox) checkboxMap.get(viewName); - checkbox.setSelected(selected); - } - - private ByteViewerOptionsDialog launchByteViewerOptions() { - final DockingActionIf action = getAction(plugin, "Byte Viewer Options"); - assertTrue(action.isEnabled()); - - runSwing(() -> action.actionPerformed(new DefaultActionContext()), false); - waitForSwing(); - ByteViewerOptionsDialog d = waitForDialogComponent(ByteViewerOptionsDialog.class); - return d; - } - - private void setView(ByteViewerComponent view) { - runSwing(() -> panel.setCurrentView(view)); - } - - private ByteViewerComponent getView(String name) { - List views = runSwing(() -> panel.getViewList()); - for (ByteViewerComponent viewer : views) { - if (viewer.getName().equals(name)) { - return viewer; - } - } - - fail("Cannot find view '" + name + "'"); - return null; - } - - private FieldLocation getFieldLocation(Address addr) { - - return runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - ProgramByteBlockSet blockset = - (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); - return c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); - }); - } - - private Address addr(long offset) { - return addr(Long.toHexString(offset)); - } - - private Address addr(String offset) { - return program.getAddressFactory().getAddress(offset); - } - - private Address convertToAddr(ByteBlockInfo info) { - return ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()) - .getAddress(info.getBlock(), info.getOffset()); - } - - private boolean byteBlockSelectionEquals(ByteBlockSelection b1, ByteBlockSelection b2) { - - int nRanges = b1.getNumberOfRanges(); - if (nRanges != b2.getNumberOfRanges()) { - return false; - } - for (int i = 0; i < nRanges; i++) { - ByteBlockRange range1 = b1.getRange(i); - ByteBlockRange range2 = b2.getRange(i); - if (!range1.equals(range2)) { - return false; - } - } - return true; - } - - private ByteViewerComponent findComponent(Container container, String name) { - Component[] c = container.getComponents(); - for (Component element : c) { - if (element instanceof ByteViewerComponent) { - if (((ByteViewerComponent) element).getDataModel().getName().equals(name)) { - return (ByteViewerComponent) element; - } - } - else if (element instanceof Container) { - ByteViewerComponent bvc = findComponent((Container) element, name); - if (bvc != null) { - return bvc; - } - } - } - return null; - } - - private void loadViews(String... viewNames) { - assertNotEquals(0, viewNames.length); - ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - for (String name : viewNames) { - setViewSelected(dialog, name, true); - } - pressButtonByText(dialog.getComponent(), "OK"); - } - - private Container findContainer(Container parent, Class theClass) { - Component[] c = parent.getComponents(); - for (Component element : c) { - if (element.getClass() == theClass) { - return (Container) element; - } - if (element instanceof Container) { - Container container = findContainer((Container) element, theClass); - if (container != null) { - return container; - } - } - } - return null; - } - - private String findLabelStr(Container container, String name) { - Component[] c = container.getComponents(); - for (Component element : c) { - if (element instanceof JLabel) { - if (name.equals(((JLabel) element).getName())) { - return ((JLabel) element).getText(); - } - } - if (element instanceof Container) { - String str = findLabelStr((Container) element, name); - if (str != null) { - return str; - } - } - } - return null; - } - - private void setViewWidth(String name, int width) { - runSwing(() -> { - panel.setViewWidth(name, width); - }); - } - - private int getViewWidth(String name) { - return runSwing(() -> { - return panel.getViewWidth("Ascii"); - }); - } } diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin2Test.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin2Test.java index 9c2d1f0a28..3b52354f8a 100644 --- a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin2Test.java +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin2Test.java @@ -22,83 +22,46 @@ import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.math.BigInteger; -import java.util.Date; import java.util.List; -import java.util.Map; -import javax.swing.JCheckBox; -import javax.swing.JLabel; +import org.junit.Assert; +import org.junit.Test; -import org.junit.*; - -import docking.DefaultActionContext; import docking.DockingUtils; import docking.action.DockingActionIf; -import docking.action.ToggleDockingAction; import docking.widgets.EventTrigger; import docking.widgets.fieldpanel.support.*; import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; -import ghidra.app.plugin.core.format.*; +import ghidra.app.plugin.core.format.ByteBlockSelection; import ghidra.app.plugin.core.navigation.*; import ghidra.app.services.GoToService; import ghidra.app.services.ProgramManager; -import ghidra.app.util.bean.FixedBitSizeValueField; import ghidra.framework.options.Options; -import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.Plugin; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.Address; import ghidra.program.model.data.ByteDataType; import ghidra.program.model.data.StructureDataType; -import ghidra.program.model.listing.Listing; import ghidra.program.model.listing.Program; -import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.util.ProgramLocation; -import ghidra.test.AbstractGhidraHeadedIntegrationTest; -import ghidra.test.TestEnv; import ghidra.util.task.TaskMonitor; import utility.function.ExceptionalCallback; /** * Test editing in the Byte Viewer. */ -public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { +public class ByteViewerPlugin2Test extends AbstractByteViewerPluginTest { - private TestEnv env; - private PluginTool tool; - private Program program; - private Memory memory; - private Listing listing; - private ByteViewerPlugin plugin; - private ByteViewerPanel panel; - private CodeBrowserPlugin cbPlugin; - - @Before - public void setUp() throws Exception { - env = new TestEnv(); - tool = env.getTool(); - tool.addPlugin(GoToAddressLabelPlugin.class.getName()); - tool.addPlugin(NavigationHistoryPlugin.class.getName()); - tool.addPlugin(NextPrevAddressPlugin.class.getName()); - tool.addPlugin(CodeBrowserPlugin.class.getName()); - - tool.addPlugin(ByteViewerPlugin.class.getName()); - - plugin = env.getPlugin(ByteViewerPlugin.class); - tool.showComponentProvider(plugin.getProvider(), true); - cbPlugin = env.getPlugin(CodeBrowserPlugin.class); - - program = buildNotepad(); - memory = program.getMemory(); - listing = program.getListing(); - ProgramManager pm = tool.getService(ProgramManager.class); - pm.openProgram(program.getDomainFile()); - panel = plugin.getProvider().getByteViewerPanel(); - waitForSwing(); + @Override + protected List> getDefaultPlugins() { + return List.of(GoToAddressLabelPlugin.class, NavigationHistoryPlugin.class, + NextPrevAddressPlugin.class, CodeBrowserPlugin.class); } - private Program buildNotepad() throws Exception { + @Override + protected Program buildProgram() throws Exception { ProgramBuilder builder = new ProgramBuilder("notepad", ProgramBuilder._TOY); builder.createMemory("test2", "0x1001000", 0x2000); builder.createMemory("test2", "0xf001000", 0x100); @@ -110,102 +73,56 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { return p; } - @After - public void tearDown() throws Exception { - env.dispose(); - } - @Test public void testEditModeHex() throws Exception { - env.showTool(); + Address addr = addr(0x01001000); + setEditMode(true); - ToggleDockingAction editAction = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - editAction.setSelected(true); - editAction.actionPerformed(new DefaultActionContext()); - }); - assertTrue(editAction.isSelected()); + goTo(addr); ByteViewerComponent c = panel.getCurrentComponent(); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); waitForSwing(); - assertEquals((byte) 0xa0, program.getMemory().getByte(getAddr(0x01001000))); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, - ((ByteField) c.getCurrentField()).getForeground()); + assertByteData(addr, 0xa0); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); - runSwing(() -> { - editAction.setSelected(false); - editAction.actionPerformed(new DefaultActionContext()); - }); - assertFalse(editAction.isSelected()); - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, - c.getFocusedCursorColor()); + setEditMode(false); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); } @Test public void testEditModeHex2() throws Exception { // verify that the byte being edited cannot be in an instruction + Address addr = addr(0x01002000); + goTo(addr); - env.showTool(); + setEditMode(true); - Address addr = getAddr(0x01002000); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(addr); - byte b = memory.getByte(addr); - assertEquals((byte) 0x50, b); - assertEquals(256, loc.getIndex().intValue()); - assertEquals(0, loc.getFieldNum()); + assertByteData(addr, 0x50); + assertFieldLocationInfo(addr, 256, 0); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); ByteViewerComponent c = panel.getCurrentComponent(); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); - - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); waitForSwing(); - assertEquals(b, program.getMemory().getByte(addr)); - Color fg = ((ByteField) c.getCurrentField()).getForeground(); - if (fg != null) { - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, fg); - } - Window w = windowForComponent(plugin.getProvider().getComponent()); - String str = findLabelStr(w, "Tool Status"); + + assertByteData(addr, 0x50); + assertFieldColor(getField(c), ByteViewerComponentProvider.FG_COLOR, true); + + Window w = windowForComponent(provider.getComponent()); + String str = runSwing(() -> findLabelStr(w, "Tool Status")); assertEquals("Editing not allowed: Instruction exists at address 01002000", str); } @Test public void testEditModeHex3() throws Exception { - env.showTool(); - - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, getAddr(01001530))); - - waitForSwing(); - - Address addr = getAddr(0x01001530); + Address addr = addr(0x01001530); + goTo(addr); txSwing(() -> { StructureDataType dt = new StructureDataType("test", 0); for (int i = 0; i < 7; i++) { @@ -214,41 +131,24 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { listing.createData(addr, dt, dt.getLength()); }); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - runSwing(() -> { - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + setEditMode(true); ByteViewerComponent c = panel.getCurrentComponent(); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); waitForSwing(); - assertEquals((byte) 0xa0, program.getMemory().getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - + assertByteData(addr, 0xa0); + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } @Test public void testEditsInOtherViews() throws Exception { // verify changed bytes show up in red in other views - env.showTool(); addViews(); - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, getAddr(01001530))); - waitForSwing(); - - Address addr = getAddr(0x01001530); + Address addr = addr(0x01001530); + goTo(addr); txSwing(() -> { StructureDataType dt = new StructureDataType("test", 0); for (int i = 0; i < 7; i++) { @@ -257,89 +157,52 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { listing.createData(addr, dt, dt.getLength()); }); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - runSwing(() -> { - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + setEditMode(true); ByteViewerComponent c = panel.getCurrentComponent(); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); waitForSwing(); - assertEquals((byte) 0xa0, program.getMemory().getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); + assertByteData(addr, 0xa0); + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertEquals("a0", getField(c, addr).getText()); - ByteViewerComponent asciiC = findComponent(panel, "Ascii"); - runSwing(() -> panel.setCurrentView(asciiC)); + assertFieldColor(getField("Chars", addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertEquals(".", getField("Chars", addr).getText()); - loc = getFieldLocation(addr); - field = asciiC.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - - ByteViewerComponent hexIntC = findComponent(panel, "Hex Integer"); - runSwing(() -> panel.setCurrentView(hexIntC)); - - loc = getFieldLocation(addr); - field = asciiC.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); + assertFieldColor(getField("Hex Integer", addr), + ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertEquals("a0000000", getField("Hex Integer", addr).getText()); } @Test public void testUndoRedo() throws Exception { - env.showTool(); - Address addr = getAddr(0x01001000); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); + Address addr = addr(0x01001000); + goTo(addr); + setEditMode(true); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent c = panel.getCurrentComponent(); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); ByteViewerComponent c = panel.getCurrentComponent(); + byte value = program.getMemory().getByte(addr); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); waitForSwing(); - assertEquals((byte) 0xa0, memory.getByte(addr)); + assertByteData(addr, 0xa0); undo(program); program.flushEvents(); waitForSwing(); - assertEquals(value, memory.getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field.getForeground()); + assertByteData(addr, value); + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.FG_COLOR, true); redo(program); program.flushEvents(); // field color should show edit color - loc = getFieldLocation(addr); - field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } @Test @@ -347,38 +210,21 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { disableCodeBrowserLocationEvents(); - env.showTool(); + goTo(addr(0x01001000)); // make 5 changes // verify that the Undo button is enabled and undo can be done 5 times - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); + setEditMode(true); - runSwing(() -> { - Address addr = getAddr(0x01001000); - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent c = panel.getCurrentComponent(); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - ByteViewerComponent c = panel.getCurrentComponent(); + char[] values = { 'a', '1', '2', 'b', '3' }; + int[] keyCodes = + { KeyEvent.VK_A, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3 }; - runSwing(() -> { - char[] values = { 'a', '1', '2', 'b', '3' }; - int[] keyCodes = - { KeyEvent.VK_P, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3 }; - - Address addr = getAddr(0x01001000); - for (int i = 0; i < 5; i++) { - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, keyCodes[i], values[i]); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - addr = addr.add(1); - } - - }); + Address addr = addr(0x01001000); + for (int i = 0; i < keyCodes.length; i++) { + pressKey(0, keyCodes[i], values[i]); + addr = addr.add(1); + } program.flushEvents(); waitForSwing(); @@ -386,313 +232,200 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { FieldLocation loc2 = new FieldLocation(0, 1, 0, 0); // second byte on first line undo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); undo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); undo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, null); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, null); undo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, null); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, null); undo(program); - testFieldColor(loc1, null); - testFieldColor(loc2, null); + assertFieldColor(loc1, null); + assertFieldColor(loc2, null); assertFalse(program.canUndo()); redo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, null); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, null); redo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, null); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, null); redo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); redo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); redo(program); - testFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - testFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc1, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(loc2, ByteViewerComponentProvider.EDITED_TEXT_COLOR); assertTrue(program.canUndo()); } @Test public void testUndoRedoHexShort() throws Exception { - env.showTool(); addViews(); - Address addr = getAddr(0x01001003); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - ByteViewerComponent c = findComponent(panel, "Hex Short"); - panel.setCurrentView(c); + Address addr = addr(0x01001003); + goTo(addr); + + ByteViewerComponent c = setView("Hex Short"); + + setEditMode(true); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol()); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); byte value = program.getMemory().getByte(addr); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = - new KeyEvent(currentComponent, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); - assertEquals((byte) 0xa0, memory.getByte(addr)); + assertByteData(addr, 0xa0); undo(program); - assertEquals(value, memory.getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field.getForeground()); + assertByteData(addr, value); + + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.FG_COLOR, true); redo(program); // field color should show edit color - loc = getFieldLocation(addr); - field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } @Test public void testUndoRedoHexInteger() throws Exception { - env.showTool(); addViews(); - Address addr = getAddr(0x01001003); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - ByteViewerComponent c = findComponent(panel, "Hex Integer"); - panel.setCurrentView(c); + Address addr = addr(0x01001003); + goTo(addr); + + ByteViewerComponent c = setView("Hex Integer"); + setEditMode(true); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol()); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); byte value = program.getMemory().getByte(addr); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = - new KeyEvent(currentComponent, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); - assertEquals((byte) 0xa0, memory.getByte(addr)); + assertByteData(addr, 0xa0); undo(program); - assertEquals(value, memory.getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field.getForeground()); + assertByteData(addr, value); + + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.FG_COLOR, true); redo(program); // field color should show edit color - loc = getFieldLocation(addr); - field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } @Test public void testUndoRedoHexLong() throws Exception { - env.showTool(); + addViews(); - Address addr = getAddr(0x01001003); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - ByteViewerComponent c = findComponent(panel, "Hex Long"); - panel.setCurrentView(c); + setEditMode(true); + Address addr = addr(0x01001003); + goTo(addr); + + ByteViewerComponent c = setView("Hex Long"); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol()); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); byte value = program.getMemory().getByte(addr); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = - new KeyEvent(currentComponent, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); - assertEquals((byte) 0xa0, memory.getByte(addr)); + assertByteData(addr, 0xa0); undo(program); - assertEquals(value, memory.getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field.getForeground()); + assertByteData(addr, value); + + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.FG_COLOR, true); redo(program); // field color should show edit color - loc = getFieldLocation(addr); - field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } @Test public void testUndoRedoHexLongLong() throws Exception { - env.showTool(); + addViews(); - Address addr = getAddr(0x01001003); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - ByteViewerComponent c = findComponent(panel, "Hex Long Long"); - panel.setCurrentView(c); + Address addr = addr(0x01001003); + goTo(addr); + setEditMode(true); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol()); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + ByteViewerComponent c = setView("Hex Long Long"); byte value = program.getMemory().getByte(addr); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = - new KeyEvent(currentComponent, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); - assertEquals((byte) 0xa0, memory.getByte(addr)); + assertByteData(addr, 0xa0); undo(program); - assertEquals(value, memory.getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field.getForeground()); + assertByteData(addr, value); + + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.FG_COLOR, true); redo(program); // field color should show edit color - loc = getFieldLocation(addr); - field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } @Test public void testUndoRedoHexShort2() throws Exception { disableCodeBrowserLocationEvents(); - env.showTool(); addViews(); - ByteViewerComponent c = findComponent(panel, "Hex Short"); - panel.setCurrentView(c); // make 3 changes // verify that the Undo button is enabled and undo can be done 5 times - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); + goTo(addr(0x01001002)); + setEditMode(true); - runSwing(() -> { - Address addr = getAddr(0x01001003); - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + ByteViewerComponent c = setView("Hex Short"); + char[] values = { 'a', '1', '2' }; + int[] keyCodes = { KeyEvent.VK_A, KeyEvent.VK_1, KeyEvent.VK_2, }; - runSwing(() -> { - char[] values = { 'a', '1', '2' }; - int[] keyCodes = - { KeyEvent.VK_P, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3 }; + for (int i = 0; i < keyCodes.length; i++) { + pressKey(0, keyCodes[i], values[i]); + } - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - for (int i = 0; i < 3; i++) { - FieldLocation loc = currentComponent.getCursorLocation(); - KeyEvent ev = new KeyEvent(currentComponent, 0, new Date().getTime(), 0, - keyCodes[i], values[i]); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - } - - }); program.flushEvents(); waitForSwing(); - for (int i = 0; i < 3; i++) { + for (int i = 0; i < keyCodes.length; i++) { assertTrue(program.canUndo()); undo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - if (i == 2) { - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field - .getForeground()); - } - else { - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - } + Color expectedColor = i == keyCodes.length - 1 + ? ByteViewerComponentProvider.FG_COLOR + : ByteViewerComponentProvider.EDITED_TEXT_COLOR; + + assertFieldColor(c.getCursorLocation(), expectedColor, + expectedColor == ByteViewerComponentProvider.FG_COLOR); } assertFalse(program.canUndo()); - for (int i = 0; i < 3; i++) { + for (int i = 0; i < keyCodes.length; i++) { redo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(c.getCursorLocation(), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } assertTrue(program.canUndo()); @@ -702,70 +435,43 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { public void testUndoRedoHexInteger2() throws Exception { disableCodeBrowserLocationEvents(); - env.showTool(); addViews(); - ByteViewerComponent c = findComponent(panel, "Hex Integer"); - panel.setCurrentView(c); + goTo(addr(0x01001000)); + setEditMode(true); + + ByteViewerComponent c = setView("Hex Integer"); + // make 5 changes // verify that the Undo button is enabled and undo can be done 5 times - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); + char[] values = { 'a', '1', '2', 'b', '3' }; + int[] keyCodes = + { KeyEvent.VK_A, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3 }; - runSwing(() -> { - Address addr = getAddr(0x01001003); - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - - runSwing(() -> { - char[] values = { 'a', '1', '2', 'b', '3' }; - int[] keyCodes = - { KeyEvent.VK_P, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3 }; - - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - for (int i = 0; i < 5; i++) { - FieldLocation loc = currentComponent.getCursorLocation(); - KeyEvent ev = new KeyEvent(currentComponent, 0, new Date().getTime(), 0, - keyCodes[i], values[i]); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - } - - }); + for (int i = 0; i < keyCodes.length; i++) { + pressKey(0, keyCodes[i], values[i]); + } program.flushEvents(); waitForSwing(); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < keyCodes.length; i++) { assertTrue(program.canUndo()); undo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - if (i == 4) { - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field - .getForeground()); - } - else { - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - } + Color expectedColor = i == keyCodes.length - 1 + ? ByteViewerComponentProvider.FG_COLOR + : ByteViewerComponentProvider.EDITED_TEXT_COLOR; + assertFieldColor(getField(c), expectedColor, + expectedColor == ByteViewerComponentProvider.FG_COLOR); } assertFalse(program.canUndo()); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < keyCodes.length; i++) { redo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } assertTrue(program.canUndo()); @@ -775,60 +481,35 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { public void testUndoRedoHexLong2() throws Exception { disableCodeBrowserLocationEvents(); - env.showTool(); addViews(); - ByteViewerComponent c = findComponent(panel, "Hex Long"); - panel.setCurrentView(c); + setEditMode(true); + + ByteViewerComponent c = setView("Hex Long"); // make 9 changes // verify that the Undo button is enabled and undo can be done 5 times - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); + char[] values = { 'a', '1', '2', 'b', '3', 'c', '4', 'd', '5' }; + int[] keyCodes = + { KeyEvent.VK_A, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3, + KeyEvent.VK_C, KeyEvent.VK_4, KeyEvent.VK_D, KeyEvent.VK_5 }; - runSwing(() -> { - Address addr = getAddr(0x01001003); - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + for (int i = 0; i < keyCodes.length; i++) { + pressKey(0, keyCodes[i], values[i]); + } - runSwing(() -> { - char[] values = { 'a', '1', '2', 'b', '3', 'c', '4', 'd', '5' }; - int[] keyCodes = - { KeyEvent.VK_P, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3, - KeyEvent.VK_C, KeyEvent.VK_4, KeyEvent.VK_D, KeyEvent.VK_5 }; - - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - for (int i = 0; i < 9; i++) { - FieldLocation loc = currentComponent.getCursorLocation(); - KeyEvent ev = new KeyEvent(currentComponent, 0, new Date().getTime(), 0, - keyCodes[i], values[i]); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - } - - }); program.flushEvents(); waitForSwing(); - for (int i = 0; i < 9; i++) { + for (int i = 0; i < keyCodes.length; i++) { assertTrue(program.canUndo()); undo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - if (i == 8) { - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field - .getForeground()); - } - else { - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - } + Color expectedColor = i == keyCodes.length - 1 + ? ByteViewerComponentProvider.FG_COLOR + : ByteViewerComponentProvider.EDITED_TEXT_COLOR; + assertFieldColor(getField(c), expectedColor, + expectedColor == ByteViewerComponentProvider.FG_COLOR); } assertFalse(program.canUndo()); @@ -836,10 +517,7 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { redo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } assertTrue(program.canUndo()); @@ -849,75 +527,45 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { public void testUndoRedoHexLongLong2() throws Exception { disableCodeBrowserLocationEvents(); - env.showTool(); + addViews(); - ByteViewerComponent c = findComponent(panel, "Hex Long Long"); - panel.setCurrentView(c); + setEditMode(true); + ByteViewerComponent c = setView("Hex Long Long"); // make 9 changes // verify that the Undo button is enabled and undo can be done 5 times - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); + char[] values = { 'a', '1', '2', 'b', '3', 'c', '4', 'd', '5', 'e', '6', 'f', '7', 'a', + '8', 'b', '9' }; + int[] keyCodes = { KeyEvent.VK_A, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, + KeyEvent.VK_3, KeyEvent.VK_C, KeyEvent.VK_4, KeyEvent.VK_D, KeyEvent.VK_5, + KeyEvent.VK_E, KeyEvent.VK_6, KeyEvent.VK_F, KeyEvent.VK_7, KeyEvent.VK_A, + KeyEvent.VK_8, KeyEvent.VK_B, KeyEvent.VK_9 }; - runSwing(() -> { - Address addr = getAddr(0x01001003); - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + for (int i = 0; i < keyCodes.length; i++) { + pressKey(0, keyCodes[i], values[i]); + } - runSwing(() -> { - char[] values = { 'a', '1', '2', 'b', '3', 'c', '4', 'd', '5', 'e', '6', 'f', '7', 'a', - '8', 'b', '9' }; - int[] keyCodes = - { KeyEvent.VK_P, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_B, KeyEvent.VK_3, - KeyEvent.VK_C, KeyEvent.VK_4, KeyEvent.VK_D, KeyEvent.VK_5, KeyEvent.VK_E, - KeyEvent.VK_6, - KeyEvent.VK_F, KeyEvent.VK_7, KeyEvent.VK_G, KeyEvent.VK_8, KeyEvent.VK_H, - KeyEvent.VK_9 }; - - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - for (int i = 0; i < 17; i++) { - FieldLocation loc = currentComponent.getCursorLocation(); - KeyEvent ev = new KeyEvent(currentComponent, 0, new Date().getTime(), 0, - keyCodes[i], values[i]); - currentComponent.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), - loc.getCol(), currentComponent.getCurrentField()); - } - - }); program.flushEvents(); waitForSwing(); - for (int i = 0; i < 17; i++) { + for (int i = 0; i < keyCodes.length; i++) { assertTrue(program.canUndo()); undo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - if (i == 16) { - assertTrue(fg == null || - ByteViewerComponentProvider.CURSOR_COLOR_UNFOCUSED_NON_EDIT == field - .getForeground()); - } - else { - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); - } + Color expectedColor = i == keyCodes.length - 1 + ? ByteViewerComponentProvider.FG_COLOR + : ByteViewerComponentProvider.EDITED_TEXT_COLOR; + assertFieldColor(getField(c), expectedColor, + expectedColor == ByteViewerComponentProvider.FG_COLOR); } assertFalse(program.canUndo()); - for (int i = 0; i < 17; i++) { + for (int i = 0; i < keyCodes.length; i++) { redo(program); - FieldLocation loc = c.getCursorLocation(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fg = field.getForeground(); - assertEquals(fg, ByteViewerComponentProvider.EDITED_TEXT_COLOR); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); } assertTrue(program.canUndo()); @@ -925,347 +573,195 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testEditInputHex() throws Exception { - env.showTool(); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); + + setEditMode(true); + Address addr = addr(0x01001000); + goTo(addr); ByteViewerComponent c = panel.getCurrentComponent(); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); - byte value = program.getMemory().getByte(getAddr(0x01001000)); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_P, 'p'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT, + c.getFocusedCursorColor()); + byte value = program.getMemory().getByte(addr); + pressKey(0, KeyEvent.VK_P, 'p'); program.flushEvents(); - assertEquals(value, program.getMemory().getByte(getAddr(0x01001000))); + assertByteData(addr, value); } @Test public void testEditModeAscii() throws Exception { - env.showTool(); - addViews(); - waitForSwing(); - ByteViewerComponent c = findComponent(panel, "Ascii"); - panel.setCurrentView(c); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - waitForSwing(); - assertTrue(action.isSelected()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + loadViews("Chars"); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_Z, 'z'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + Address addr = addr(0x01001000); + goTo(addr); + setEditMode(true); + + ByteViewerComponent c = setView("Chars"); + + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); + assertFieldColor(getField(c), ByteViewerComponentProvider.FG_COLOR, true); + + pressKey(0, KeyEvent.VK_Z, 'z'); program.flushEvents(); - Address addr = getAddr(0x01001000); - assertEquals((byte) 0x7a, program.getMemory().getByte(addr)); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); + assertByteData(addr, 0x7a); - runSwing(() -> { - action.setSelected(false); - action.actionPerformed(new DefaultActionContext()); - }); - assertFalse(action.isSelected()); - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, - c.getFocusedCursorColor()); + assertEquals("z", getField(c, addr).getText()); + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); + + // cursor should have auto-moved, check new field color + assertFieldColor(getField(c), ByteViewerComponentProvider.FG_COLOR, true); + + setEditMode(false); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); } @Test public void testEditModeOctal() throws Exception { - env.showTool(); addViews(); - waitForSwing(); - ByteViewerComponent c = findComponent(panel, "Octal"); - panel.setCurrentView(c); - Address addr = getAddr(0x01001000); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + ByteViewerComponent c = setView("Octal"); - runSwing(() -> { - FieldLocation loc = getFieldLocation(addr); - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_1, '1'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + Address addr = addr(0x01001000); + goTo(addr); + setEditMode(true); + + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); + + pressKey(0, KeyEvent.VK_1, '1'); program.flushEvents(); - assertEquals((byte) 0x40, program.getMemory().getByte(addr)); - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - - runSwing(() -> { - action.setSelected(false); - action.actionPerformed(new DefaultActionContext()); - }); - assertFalse(action.isSelected()); - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, - c.getFocusedCursorColor()); + assertByteData(addr, 0x40); + assertFieldColor(getField(c, addr), ByteViewerComponentProvider.EDITED_TEXT_COLOR); + setEditMode(false); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); } @Test public void testEditInputOctal() throws Exception { - env.showTool(); addViews(); - waitForSwing(); - ByteViewerComponent c = findComponent(panel, "Octal"); - panel.setCurrentView(c); + ByteViewerComponent c = setView("Octal"); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + Address addr = addr(0x01001000); + goTo(addr); + setEditMode(true); - byte value = program.getMemory().getByte(getAddr(0x01001000)); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_9, '9'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); + + byte value = program.getMemory().getByte(addr); + pressKey(0, KeyEvent.VK_9, '9'); // '9' is out of range for octal program.flushEvents(); - assertEquals(value, program.getMemory().getByte(getAddr(0x01001000))); + assertByteData(addr, value); } @Test public void testEditModeHexShort() throws Exception { - env.showTool(); addViews(); + ByteViewerComponent c = setView("Hex Short"); - ByteViewerComponent c = findComponent(panel, "Hex Short"); - panel.setCurrentView(c); + Address addr = addr(0x01001003); + goTo(addr); + setEditMode(true); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001003)); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_1, '1'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_1, '1'); program.flushEvents(); - assertEquals((byte) 0x10, program.getMemory().getByte(getAddr(0x01001003))); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, - ((ByteField) c.getCurrentField()).getForeground()); + assertByteData(addr, 0x10); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); - runSwing(() -> { - action.setSelected(false); - action.actionPerformed(new DefaultActionContext()); - }); - assertFalse(action.isSelected()); - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, - c.getFocusedCursorColor()); + setEditMode(false); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); } @Test public void testEditModeHexInteger() throws Exception { - env.showTool(); addViews(); + ByteViewerComponent c = setView("Hex Integer"); - ByteViewerComponent c = findComponent(panel, "Hex Integer"); - panel.setCurrentView(c); + Address addr = addr(0x01001003); + goTo(addr); + setEditMode(true); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001003)); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_1, '1'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_1, '1'); program.flushEvents(); - assertEquals((byte) 0x10, program.getMemory().getByte(getAddr(0x01001003))); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, - ((ByteField) c.getCurrentField()).getForeground()); + assertByteData(addr, 0x10); - runSwing(() -> { - action.setSelected(false); - action.actionPerformed(new DefaultActionContext()); - }); - assertFalse(action.isSelected()); - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, - c.getFocusedCursorColor()); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); + + setEditMode(false); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); } @Test public void testEditModeHexLong() throws Exception { - env.showTool(); addViews(); + ByteViewerComponent c = setView("Hex Long"); - ByteViewerComponent c = findComponent(panel, "Hex Long"); - panel.setCurrentView(c); + Address addr = addr(0x01001003); + goTo(addr); + setEditMode(true); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001003)); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_1, '1'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_1, '1'); program.flushEvents(); - assertEquals((byte) 0x10, program.getMemory().getByte(getAddr(0x01001003))); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, - ((ByteField) c.getCurrentField()).getForeground()); + assertByteData(addr, 0x10); - runSwing(() -> { - action.setSelected(false); - action.actionPerformed(new DefaultActionContext()); - }); - assertFalse(action.isSelected()); - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, - c.getFocusedCursorColor()); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); + + setEditMode(false); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); } @Test public void testEditModeHexLongLong() throws Exception { - env.showTool(); addViews(); + ByteViewerComponent c = setView("Hex Long Long"); - ByteViewerComponent c = findComponent(panel, "Hex Long Long"); - panel.setCurrentView(c); + Address addr = addr(0x01001003); + goTo(addr); + setEditMode(true); - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - FieldLocation loc = getFieldLocation(getAddr(0x01001003)); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); - assertTrue(action.isSelected()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, c.getFocusedCursorColor()); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_EDIT); - runSwing(() -> { - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_1, '1'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + pressKey(0, KeyEvent.VK_1, '1'); program.flushEvents(); - assertEquals((byte) 0x10, program.getMemory().getByte(getAddr(0x01001003))); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, - ((ByteField) c.getCurrentField()).getForeground()); + assertByteData(addr, 0x10); - runSwing(() -> { - action.setSelected(false); - action.actionPerformed(new DefaultActionContext()); - }); - assertFalse(action.isSelected()); - assertEquals(ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT, - c.getFocusedCursorColor()); + assertFieldColor(getField(c), ByteViewerComponentProvider.EDITED_TEXT_COLOR); + + setEditMode(false); + assertCursorColor(c, ByteViewerComponentProvider.CURSOR_COLOR_FOCUSED_NON_EDIT); } @Test public void testMemoryMapEdits() throws Exception { - env.showTool(); - // put the byte viewer in edit mode - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - runSwing(() -> { - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + setEditMode(true); + + Address addr = addr(0x100); // add a memory block txSwing(() -> { - Address addr = getAddr(0x100); memory.createInitializedBlock(".test", addr, 10, (byte) 0, null, false); }); // edit the first 3 bytes in the block ByteViewerComponent c = panel.getCurrentComponent(); - runSwing(() -> { - Address testAddress = getAddr(0x100); - FieldLocation loc = getFieldLocation(testAddress); - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_1, '1'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); + char[] values = { '1', '2', '3' }; + int[] keyCodes = { KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3 }; - testAddress = testAddress.add(1); - loc = getFieldLocation(testAddress); - ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_2, '2'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - - testAddress = testAddress.add(1); - loc = getFieldLocation(testAddress); - ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_3, '3'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + for (int i = 0; i < keyCodes.length; i++) { + goTo(addr.add(i)); + pressKey(0, keyCodes[i], values[i]); + } program.flushEvents(); waitForSwing(); - Address addr = getAddr(0x100); for (int i = 0; i < 3; i++) { // verify that the bytes are rendered in red - FieldLocation loc = getFieldLocation(addr); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertEquals(ByteViewerComponentProvider.EDITED_TEXT_COLOR, field.getForeground()); - addr = addr.add(i); + assertFieldColor(getField(c, addr.add(i)), + ByteViewerComponentProvider.EDITED_TEXT_COLOR); } } @@ -1273,69 +769,45 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { public void testMemoryMapMove() throws Exception { // Move a memory block; verify that the byte viewer updates - env.showTool(); - - // put the byte viewer in edit mode - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - runSwing(() -> { - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + setEditMode(true); // first add a memory block txSwing(() -> { - Address addr = getAddr(0x100); - memory.createInitializedBlock(".test", addr, 10, (byte) 0, null, false); + memory.createInitializedBlock(".test", addr(0x100), 10, (byte) 0, null, false); }); // edit the first 3 bytes in the block ByteViewerComponent c = panel.getCurrentComponent(); - runSwing(() -> { - Address testAddress = getAddr(0x100); - FieldLocation loc = getFieldLocation(testAddress); - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_1, '1'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); + char[] values = { '1', '2', '3' }; + int[] keyCodes = { KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3 }; - testAddress = testAddress.add(1); - loc = getFieldLocation(testAddress); - ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_2, '2'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - - testAddress = testAddress.add(1); - loc = getFieldLocation(testAddress); - ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_3, '3'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + for (int i = 0; i < keyCodes.length; i++) { + goTo(addr(0x100).add(i)); + pressKey(0, keyCodes[i], values[i]); + } program.flushEvents(); + waitForSwing(); // move the block txSwing(() -> { - Address addr = getAddr(0x100); - MemoryBlock block = memory.getBlock(addr); - Address newStart = getAddr(0x500); - memory.moveBlock(block, newStart, TaskMonitor.DUMMY); + MemoryBlock block = memory.getBlock(addr(0x100)); + memory.moveBlock(block, addr(0x500), TaskMonitor.DUMMY); }); - Address addr = getAddr(0x500); - FieldLocation loc = getFieldLocation(addr); - - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - assertNotNull(field); + assertEquals("10", getField(c, addr(0x500)).getText()); + assertEquals("20", getField(c, addr(0x501)).getText()); + assertEquals("30", getField(c, addr(0x502)).getText()); } @Test public void testMemoryBlockSplit() throws Exception { // split a memory block; verify that the byte viewer shows // the separator between blocks after the block is split - env.showTool(); - Address addr = getAddr(0x01002000); + + Address addr = addr(0x01002000); MemoryBlock block = memory.getBlock(addr); - Address newaddr = getAddr(0x01002009); + Address newaddr = addr(0x01002009); txSwing(() -> { memory.split(block, newaddr); @@ -1355,13 +827,12 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { disableCodeBrowserLocationEvents(); - env.showTool(); - Address addr = getAddr(0x01001000); - MemoryBlock block = memory.getBlock(addr); - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, addr)); - Address newaddr = getAddr(0x01000500); + Address addr = addr(0x01001000); + MemoryBlock block = memory.getBlock(addr); + gotoInPanel(addr); + + Address newaddr = addr(0x01000500); txSwing(() -> { MemoryBlock newblock = @@ -1388,13 +859,12 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { disableCodeBrowserLocationEvents(); // expand block down: create a block, then join to its predecessor - env.showTool(); - Address addr = getAddr(0x01001000); - MemoryBlock block = memory.getBlock(addr); - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, addr)); - Address newaddr = getAddr(0x01003000L); + Address addr = addr(0x01001000); + MemoryBlock block = memory.getBlock(addr); + gotoInPanel(addr); + + Address newaddr = addr(0x01003000L); txSwing(() -> { MemoryBlock newblock = @@ -1414,9 +884,10 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testMemoryBlockDeletedInView() throws Exception { // delete a memory block that is showing in the view - disableCodeBrowserLocationEvents(); env.showTool(); - Address addr = getAddr(0x0f001000); + disableCodeBrowserLocationEvents(); + + Address addr = addr(0x0f001000); MemoryBlock block = memory.getBlock(addr); GoToService goToService = tool.getService(GoToService.class); goToService.goTo(new ProgramLocation(program, addr)); @@ -1429,8 +900,9 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { txSwing(() -> { memory.removeBlock(block, TaskMonitor.DUMMY); }); + waitForSwing(); - addr = getAddr(0x0f002000); + addr = addr(0x0f002000); loc = getFieldLocation(addr); // cursor should be positioned at next address after deleted block assertEquals(loc, c.getCursorLocation()); @@ -1442,17 +914,15 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { disableCodeBrowserLocationEvents(); - env.showTool(); - Address addr = getAddr(0x0f001000); - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, addr)); - waitForSwing(); + + Address addr = addr(0x0f001000); + gotoInPanel(addr); ByteViewerComponent c = panel.getCurrentComponent(); FieldLocation loc = c.getCursorLocation(); assertEquals(getFieldLocation(addr), loc); - MemoryBlock block = memory.getBlock(getAddr(0x01001000)); + MemoryBlock block = memory.getBlock(addr(0x01001000)); txSwing(() -> { memory.removeBlock(block, TaskMonitor.DUMMY); }); @@ -1470,44 +940,42 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { // position the cursor on the first byte on the line for address 0x48 // verify that the insertion label shows 0x48 - env.showTool(); + ByteViewerOptionsDialog d = launchByteViewerOptions(); - FixedBitSizeValueField field = - (FixedBitSizeValueField) getInstanceField("bytesPerLineField", d); - runSwing(() -> field.setValue(BigInteger.valueOf(10))); + runSwing(() -> d.setBytesPerLine(10)); pressButtonByText(d.getComponent(), "OK"); // add the block txSwing(() -> { - Address addr = getAddr(0x30); + Address addr = addr(0x30); memory.createInitializedBlock(".test", addr, 0x12, (byte) 0, null, false); // add the second block at 0x48 - Address addr2 = getAddr(0x48); + Address addr2 = addr(0x48); memory.createInitializedBlock(".test2", addr2, 0x100, (byte) 0, null, false); }); runSwing(() -> { ByteViewerComponent c = panel.getCurrentComponent(); - Address a = getAddr(0x48); + Address a = addr(0x48); FieldLocation loc = getFieldLocation(a); c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); }); - Address addr = getAddr(0x48); + Address addr = addr(0x48); FieldLocation loc = getFieldLocation(addr); ByteViewerComponent c = panel.getCurrentComponent(); // cursor position should not be affected assertEquals(loc, c.getCursorLocation()); - assertEquals("00000048", findLabelStr(plugin.getProvider().getComponent(), "Insertion")); + assertEquals("00000048", findLabelStr(provider.getComponent(), "Insertion")); } @Test public void testMemoryChange() throws Exception { - env.showTool(); - runSwing(() -> tool.showComponentProvider(plugin.getProvider(), false)); + + runSwing(() -> tool.showComponentProvider(provider, false)); ProgramManager pm = tool.getService(ProgramManager.class); runSwing(() -> pm.closeProgram(), false); waitForSwing(); @@ -1516,30 +984,27 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { // add a memory block txSwing(() -> { - memory.createInitializedBlock(".test", getAddr(0), 500, (byte) 0, TaskMonitor.DUMMY, + memory.createInitializedBlock(".test", addr(0), 500, (byte) 0, TaskMonitor.DUMMY, false); }); - runSwing(() -> { - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, getAddr(0))); - }); - runSwing(() -> tool.showComponentProvider(plugin.getProvider(), true)); + gotoInPanel(addr(0)); + runSwing(() -> tool.showComponentProvider(provider, true)); waitForSwing(); ByteViewerComponent c = panel.getCurrentComponent(); - assertEquals(getFieldLocation(getAddr(0)), c.getCursorLocation()); + assertEquals(getFieldLocation(addr(0)), c.getCursorLocation()); - runSwing(() -> tool.showComponentProvider(plugin.getProvider(), false)); + runSwing(() -> tool.showComponentProvider(provider, false)); waitForSwing(); // now remove the block txSwing(() -> { - MemoryBlock block = memory.getBlock(getAddr(0)); + MemoryBlock block = memory.getBlock(addr(0)); memory.removeBlock(block, TaskMonitor.DUMMY); }); - runSwing(() -> tool.showComponentProvider(plugin.getProvider(), true)); + runSwing(() -> tool.showComponentProvider(provider, true)); waitForSwing(); c = panel.getCurrentComponent(); @@ -1549,38 +1014,29 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testSelectionAcrossBlocks() throws Exception { - env.showTool(); - ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - setViewSelected(dialog, "Ascii", true); - setViewSelected(dialog, "Octal", true); - pressButtonByText(dialog.getComponent(), "OK"); + + loadViews("Chars", "Octal"); ByteViewerOptionsDialog d = launchByteViewerOptions(); - FixedBitSizeValueField field = - (FixedBitSizeValueField) getInstanceField("bytesPerLineField", d); - runSwing(() -> field.setValue(BigInteger.valueOf(10))); + runSwing(() -> d.setBytesPerLine(10)); pressButtonByText(d.getComponent(), "OK"); // add the block txSwing(() -> { - Address addr = getAddr(0x30); + Address addr = addr(0x30); memory.createInitializedBlock(".test", addr, 0x12, (byte) 0, null, false); // add the second block at 0x48 - Address addr2 = getAddr(0x48); + Address addr2 = addr(0x48); memory.createInitializedBlock(".test2", addr2, 0x100, (byte) 0, null, false); }); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - Address a = getAddr(0x30); - FieldLocation loc = getFieldLocation(a); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); - }); + goTo(addr(0x30)); + FieldSelection fsel = new FieldSelection(); ByteViewerComponent c = panel.getCurrentComponent(); FieldLocation loc1 = c.getCursorLocation(); - FieldLocation loc2 = getFieldLocation(getAddr(0x000000cb)); + FieldLocation loc2 = getFieldLocation(addr(0x000000cb)); fsel.addRange(new FieldLocation(loc1.getIndex(), loc1.getFieldNum(), 0, 0), new FieldLocation(loc2.getIndex(), loc2.getFieldNum(), 0, 0)); @@ -1590,72 +1046,46 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { }); c = panel.getCurrentComponent(); ByteBlockSelection sel = c.getViewerSelection(); - ByteViewerComponent octalC = findComponent(panel, "Octal"); + ByteViewerComponent octalC = getView("Octal"); assertTrue(byteBlockSelectionEquals(sel, octalC.getViewerSelection())); - ByteViewerComponent asciiC = findComponent(panel, "Ascii"); + ByteViewerComponent asciiC = getView("Chars"); assertTrue(byteBlockSelectionEquals(sel, asciiC.getViewerSelection())); - } @Test public void testEditLastByteInBlock() throws Exception { - env.showTool(); - - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }, false); + goTo(addr(0x01001000)); ByteViewerOptionsDialog d = launchByteViewerOptions(); - FixedBitSizeValueField field = - (FixedBitSizeValueField) getInstanceField("bytesPerLineField", d); - runSwing(() -> field.setValue(BigInteger.valueOf(10))); + runSwing(() -> d.setBytesPerLine(10)); pressButtonByText(d.getComponent(), "OK"); - runSwing(() -> { - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - }); + + setEditMode(true); // add the block txSwing(() -> { - Address addr = getAddr(0x30); + Address addr = addr(0x30); memory.createInitializedBlock(".test", addr, 0x12, (byte) 0, null, false); // add the second block at 0x48 - Address addr2 = getAddr(0x48); + Address addr2 = addr(0x48); memory.createInitializedBlock(".test2", addr2, 0x100, (byte) 0, null, false); }); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - Address a = getAddr(0x41); - FieldLocation loc = getFieldLocation(a); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); - - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_3, '3'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + goTo(addr(0x41)); + pressKey(0, KeyEvent.VK_3, '3'); program.flushEvents(); waitForSwing(); - assertEquals((byte) 0x30, memory.getByte(getAddr(0x41))); + assertByteData(addr(0x41), 0x30); } @Test public void testEditOptionCurrentViewColor() throws Exception { - env.showTool(); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); + goTo(addr(0x01001000)); Options opt = tool.getOptions("ByteViewer"); // change the color for Current View Cursor Color @@ -1668,143 +1098,102 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testEditOptionCursorColor() throws Exception { - env.showTool(); + addViews(); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); + goTo(addr(0x01001000)); Options opt = tool.getOptions("ByteViewer"); // change the color for Current View Cursor Color putColor(opt, ByteViewerComponentProvider.CURSOR_UNFOCUSED_COLOR_OPTION_NAME, Palette.GREEN); - ByteViewerComponent c = findComponent(panel, "Octal"); + ByteViewerComponent c = getView("Octal"); assertColorsEqual(Palette.GREEN, c.getNonFocusCursorColor()); } @Test public void testEditOptionNonHighlightCursorColor() throws Exception { - env.showTool(); + addViews(); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); + goTo(addr(0x01001000)); Options opt = tool.getOptions("ByteViewer"); // change the color for Current View Cursor Color putColor(opt, ByteViewerComponentProvider.CURSOR_UNFOCUSED_COLOR_OPTION_NAME, Palette.CYAN); - ByteViewerComponent c = findComponent(panel, "Octal"); + ByteViewerComponent c = getView("Octal"); assertColorsEqual(Palette.CYAN, c.getNonFocusCursorColor()); } @Test public void testEditOptionEditColor() throws Exception { // color for changed bytes - env.showTool(); + addViews(); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); + goTo(addr(0x01001000)); Options opt = tool.getOptions("ByteViewer"); // change the color for Edit Color putColor(opt, ByteViewerComponentProvider.EDIT_TEXT_COLOR_OPTION_NAME, Palette.GREEN); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - runSwing(() -> { - ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - action.setSelected(true); - action.actionPerformed(new DefaultActionContext()); - - ByteViewerComponent c = panel.getCurrentComponent(); - - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - - KeyEvent ev = new KeyEvent(c, 0, new Date().getTime(), 0, KeyEvent.VK_A, 'a'); - c.keyPressed(ev, loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol(), - c.getCurrentField()); - }); + setEditMode(true); + pressKey(0, KeyEvent.VK_A, 'a'); program.flushEvents(); waitForSwing(); - ByteViewerComponent c = panel.getCurrentComponent(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); + ByteField field = getField(getCurrentView(), addr(0x01001000)); assertColorsEqual(Palette.GREEN, field.getForeground()); } @Test public void testEditOptionFont() throws Exception { - env.showTool(); - addViews(); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); + addViews(); + goTo(addr(0x01001000)); Options opt = tool.getOptions("ByteViewer"); // change the Font Font font = new Font("Times New Roman", Font.BOLD, 12); putFont(opt, ByteViewerComponentProvider.OPTION_FONT, font); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - ByteViewerComponent c = panel.getCurrentComponent(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); + ByteField field = getField(getCurrentView(), addr(0x01001000)); assertEquals(font, field.getFontMetrics().getFont()); } @Test public void testEditOptionSeparatorColor() throws Exception { - env.showTool(); - addViews(); - runSwing(() -> { - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); + addViews(); + goTo(addr(0x01001000)); Options opt = tool.getOptions("ByteViewer"); // change the color for block separator putColor(opt, ByteViewerComponentProvider.SEPARATOR_COLOR_OPTION_NAME, Palette.GREEN); ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x0f001000)); + FieldLocation loc = getFieldLocation(addr(0x0f001000)); ByteField field = c.getField(loc.getIndex().subtract(BigInteger.ONE), 0); assertColorsEqual(Palette.GREEN, field.getForeground()); } @Test public void testGoToUpdatesByteViewer() throws Exception { - env.showTool(); - runSwing(() -> { - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, getAddr(0x01001050))); - }); + + goTo(addr(0x01001050)); ByteViewerComponent c = panel.getCurrentComponent(); - assertEquals(getFieldLocation(getAddr(0x01001050)), c.getCursorLocation()); + assertEquals(getFieldLocation(addr(0x01001050)), c.getCursorLocation()); } @Test public void testGoToUpdatesFromViewer() throws Exception { - env.showTool(); - Address address = getAddr(0x01002000); - setByteViewerLocation(plugin.getProvider(), address); + + Address address = addr(0x01002000); + setByteViewerLocation(provider, address); ProgramLocation cbLocation = cbPlugin.getCurrentLocation(); assertEquals(address, cbLocation.getAddress()); @@ -1812,12 +1201,12 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testGoToFromSnapshot() throws Exception { - env.showTool(); + DockingActionIf snapshotAction = getAction(plugin, "ByteViewer Clone"); performAction(snapshotAction, true); - Address address = getAddr(0x01002000); + Address address = addr(0x01002000); ProgramByteViewerComponentProvider snapshot = getSnapshotProvider(); setByteViewerLocation(snapshot, address); @@ -1838,7 +1227,7 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testHighlightByte() throws Exception { - env.showTool(); + ByteViewerComponent component = panel.getCurrentComponent(); ByteViewerHighlighter highlightProvider = @@ -1846,15 +1235,14 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { String currentHighlightText = highlightProvider.getText(); assertNull(currentHighlightText); - Address address = getAddr(0x01002000); - setByteViewerLocation(plugin.getProvider(), address); + Address address = addr(0x01002000); + gotoInPanel(address); int modifiers = 0; - ProgramByteViewerComponentProvider provider = plugin.getProvider(); - clickLocation(provider, modifiers); clickMiddleMouseLocation(provider, modifiers); + waitForSwing(); - currentHighlightText = highlightProvider.getText(); + currentHighlightText = runSwing(() -> highlightProvider.getText()); assertEquals("50", currentHighlightText); clickMiddleMouseLocation(provider, modifiers); @@ -1864,45 +1252,35 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testSaveRestoreState() throws Exception { - env.showTool(); - addViews(); - ByteViewerComponent c = panel.getCurrentComponent(); - FieldLocation loc = getFieldLocation(getAddr(0x0100100b)); - runSwing(() -> { - ByteViewerComponent currentComponent = panel.getCurrentComponent(); - currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); - }); + addViews(); + goTo(addr(0x0100100b)); ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - FixedBitSizeValueField field = - (FixedBitSizeValueField) getInstanceField("groupSizeField", dialog); - runSwing(() -> field.setValue(BigInteger.valueOf(4))); + runSwing(() -> dialog.setHexGroupSize(4)); pressButtonByText(dialog.getComponent(), "OK"); // change the font Options opt = tool.getOptions("ByteViewer"); Font font = new Font("Times New Roman", Font.BOLD, 12); - runSwing(() -> { + runSwing(() -> opt.setFont(ByteViewerComponentProvider.OPTION_FONT, font)); + goTo(addr(0x01002500)); - opt.setFont(ByteViewerComponentProvider.OPTION_FONT, font); - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, getAddr(0x01002500))); - }); - assertEquals(4, plugin.getProvider().getGroupSize()); + ByteViewerConfigOptions configOptions = provider.getConfigOptions(); + assertEquals(4, configOptions.getHexGroupSize()); ViewerPosition vp = panel.getViewerPosition(); runSwing(() -> env.saveRestoreToolState()); waitForSwing(); - c = panel.getCurrentComponent(); - assertEquals(4, plugin.getProvider().getGroupSize()); - assertEquals(getFieldLocation(getAddr(0x01002500)), c.getCursorLocation()); - assertEquals(font, panel.getFontMetrics().getFont()); - waitForSwing(); + configOptions = provider.getConfigOptions(); + assertEquals(4, configOptions.getHexGroupSize()); + assertEquals(getFieldLocation(addr(0x01002500)), getCurrentView().getCursorLocation()); + assertEquals(font, panel.getFont()); + assertEquals(font, getCurrentView().getFont()); assertEquals(vp, panel.getViewerPosition()); } @@ -1912,13 +1290,6 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { runSwing(() -> tool.removePlugins(List.of(cbPlugin))); } - private void testFieldColor(FieldLocation loc, Color color) { - ByteViewerComponent c = panel.getCurrentComponent(); - ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); - Color fieldColor = field.getForeground(); - assertEquals(fieldColor, color); - } - // perform the given callback in a transaction on the Swing thread private void txSwing(ExceptionalCallback c) { @@ -1973,20 +1344,6 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { clickMouse(component, MouseEvent.BUTTON1, x, y, 1, mouseModifiers); } - private void setByteViewerLocation(ProgramByteViewerComponentProvider testProvider, - Address address) throws Exception { - ByteViewerPanel byteViewerPanel = testProvider.getByteViewerPanel(); - ByteViewerComponent component = byteViewerPanel.getCurrentComponent(); - FieldLocation byteViewerLocation = getFieldLocation(address); - - runSwing(() -> { - component.setCursorPosition(byteViewerLocation.getIndex(), - byteViewerLocation.getFieldNum(), byteViewerLocation.getRow(), - byteViewerLocation.getCol()); - component.scrollToCursor(); - }); - } - private ProgramByteViewerComponentProvider getSnapshotProvider() { @SuppressWarnings("unchecked") java.util.List providers = @@ -1996,95 +1353,7 @@ public class ByteViewerPlugin2Test extends AbstractGhidraHeadedIntegrationTest { return providers.get(0); } - private ByteViewerOptionsDialog launchByteViewerOptions() { - DockingActionIf action = getAction(plugin, "Byte Viewer Options"); - assertTrue(action.isEnabled()); - - performAction(action, false); - waitForSwing(); - ByteViewerOptionsDialog d = waitForDialogComponent(ByteViewerOptionsDialog.class); - return d; - } - - private ByteViewerComponent findComponent(Container container, String name) { - Component[] c = container.getComponents(); - for (Component element : c) { - if (element instanceof ByteViewerComponent) { - if (((ByteViewerComponent) element).getDataModel().getName().equals(name)) { - return (ByteViewerComponent) element; - } - } - else if (element instanceof Container) { - ByteViewerComponent bvc = findComponent((Container) element, name); - if (bvc != null) { - return bvc; - } - } - } - return null; - } - private void addViews() throws Exception { - ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - setViewSelected(dialog, "Ascii", true); - setViewSelected(dialog, "Octal", true); - setViewSelected(dialog, "Hex Short", true); - setViewSelected(dialog, "Hex Integer", true); - setViewSelected(dialog, "Hex Long", true); - setViewSelected(dialog, "Hex Long Long", true); - pressButtonByText(dialog.getComponent(), "OK"); + loadViews("Chars", "Octal", "Hex Short", "Hex Integer", "Hex Long", "Hex Long Long"); } - - private void setViewSelected(ByteViewerOptionsDialog dialog, String viewName, - boolean selected) { - Map checkboxMap = (Map) getInstanceField("checkboxMap", dialog); - JCheckBox checkbox = (JCheckBox) checkboxMap.get(viewName); - checkbox.setSelected(selected); - } - - private Address getAddr(long offset) { - return program.getMinAddress().getNewAddress(offset); - } - - private FieldLocation getFieldLocation(Address addr) { - ByteViewerComponent c = panel.getCurrentComponent(); - ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); - return c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); - } - - private String findLabelStr(Container container, String name) { - Component[] c = container.getComponents(); - for (Component element : c) { - if (element instanceof JLabel) { - if (name.equals(element.getName())) { - return ((JLabel) element).getText(); - } - } - if (element instanceof Container) { - String str = findLabelStr((Container) element, name); - if (str != null) { - return str; - } - } - } - return null; - } - - private boolean byteBlockSelectionEquals(ByteBlockSelection b1, ByteBlockSelection b2) { - - int nRanges = b1.getNumberOfRanges(); - if (nRanges != b2.getNumberOfRanges()) { - return false; - } - for (int i = 0; i < nRanges; i++) { - ByteBlockRange range1 = b1.getRange(i); - ByteBlockRange range2 = b2.getRange(i); - if (!range1.equals(range2)) { - return false; - } - } - return true; - } - } diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin3Test.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin3Test.java index f595c14576..ff79ad1b86 100644 --- a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin3Test.java +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin3Test.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,86 +15,42 @@ */ package ghidra.app.plugin.core.byteviewer; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.awt.Point; +import java.util.List; import javax.swing.SwingUtilities; -import org.junit.*; +import org.junit.Test; import docking.widgets.fieldpanel.support.FieldLocation; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.format.ByteBlockInfo; import ghidra.app.plugin.core.format.ByteBlockSelection; import ghidra.app.plugin.core.navigation.*; -import ghidra.app.services.GoToService; -import ghidra.app.services.ProgramManager; -import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.Plugin; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSet; import ghidra.program.model.listing.CodeUnit; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramSelection; -import ghidra.test.AbstractGhidraHeadedIntegrationTest; -import ghidra.test.TestEnv; /** * Tests for what the byte viewer should do and not when it is not visible. * */ -public class ByteViewerPlugin3Test extends AbstractGhidraHeadedIntegrationTest { - private TestEnv env; - private PluginTool tool; - private Program program; - private ByteViewerPlugin plugin; - private ByteViewerPanel panel; - private CodeBrowserPlugin cbPlugin; +public class ByteViewerPlugin3Test extends AbstractByteViewerPluginTest { - /** - * Constructor for ByteViewerPlugin4Test. - * @param arg0 - */ - public ByteViewerPlugin3Test() { - super(); + @Override + protected List> getDefaultPlugins() { + return List.of(GoToAddressLabelPlugin.class, NavigationHistoryPlugin.class, + NextPrevAddressPlugin.class, CodeBrowserPlugin.class); } - /* - * @see TestCase#setUp() - */ - @Before - public void setUp() throws Exception { - env = new TestEnv(); - try { - tool = env.getTool(); - tool.addPlugin(GoToAddressLabelPlugin.class.getName()); - tool.addPlugin(NavigationHistoryPlugin.class.getName()); - tool.addPlugin(NextPrevAddressPlugin.class.getName()); - tool.addPlugin(CodeBrowserPlugin.class.getName()); - - tool.addPlugin(ByteViewerPlugin.class.getName()); - - plugin = env.getPlugin(ByteViewerPlugin.class); - tool.showComponentProvider(plugin.getProvider(), true); - cbPlugin = env.getPlugin(CodeBrowserPlugin.class); - - program = buildNotepad(); - ProgramManager pm = tool.getService(ProgramManager.class); - pm.openProgram(program.getDomainFile()); - panel = plugin.getProvider().getByteViewerPanel(); - waitForSwing(); - env.showTool(); - - } - catch (Exception e) { - env.dispose(); - throw e; - } - } - - private Program buildNotepad() throws Exception { + @Override + protected Program buildProgram() throws Exception { ProgramBuilder builder = new ProgramBuilder("notepad", ProgramBuilder._TOY); builder.createMemory("test2", "0x1001000", 0x2000); Program p = builder.getProgram(); @@ -102,32 +58,20 @@ public class ByteViewerPlugin3Test extends AbstractGhidraHeadedIntegrationTest { return p; } - /* - * @see TestCase#tearDown() - */ - @After - public void tearDown() throws Exception { - env.release(program); - env.dispose(); - } -@Test + @Test public void testSetVisible() throws Exception { - GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(getAddr(0x01001004)); - final ByteViewerComponent c = panel.getCurrentComponent(); + goTo(addr(0x01001004)); + ByteViewerComponent c = panel.getCurrentComponent(); ByteBlockInfo info = c.getViewerCursorLocation(); assertEquals(cbPlugin.getCurrentAddress(), convertToAddr(info)); // make a selection in the Code Browser Point startPoint = c.getCursorPoint(); - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - FieldLocation loc = getFieldLocation(getAddr(0x010010bc)); - c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); - } + runSwing(() -> { + FieldLocation loc = getFieldLocation(addr(0x010010bc)); + c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); }); Point endPoint = c.getCursorPoint(); @@ -140,7 +84,7 @@ public class ByteViewerPlugin3Test extends AbstractGhidraHeadedIntegrationTest { // convert bsel to an address set AddressSet set = - ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()).getAddressSet(bsel); + ((ProgramByteBlockSet) provider.getByteBlockSet()).getAddressSet(bsel); assertTrue(psel.hasSameAddresses(set)); info = c.getViewerCursorLocation(); @@ -160,50 +104,28 @@ public class ByteViewerPlugin3Test extends AbstractGhidraHeadedIntegrationTest { bsel = panel.getViewerSelection(); // convert bsel to an address set - set = ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()).getAddressSet(bsel); + set = ((ProgramByteBlockSet) provider.getByteBlockSet()).getAddressSet(bsel); assertTrue(psel.hasSameAddresses(set)); } -@Test + @Test public void testNotVisible() throws Exception { - GoToService goToService = tool.getService(GoToService.class); - Address addr = getAddr(0x01002000); - goToService.goTo(addr); - final ByteViewerComponent c = panel.getCurrentComponent(); + Address addr = addr(0x01002000); + goTo(addr); + ByteViewerComponent c = panel.getCurrentComponent(); ByteBlockInfo info = c.getViewerCursorLocation(); assertEquals(cbPlugin.getCurrentAddress(), convertToAddr(info)); showComponent(false); - addr = getAddr(0x01002500); - goToService.goTo(addr); + addr = addr(0x01002500); + goTo(addr); showComponent(true); info = c.getViewerCursorLocation(); assertEquals(addr, convertToAddr(info)); } - private Address getAddr(long offset) { - return program.getMinAddress().getNewAddress(offset); - } - - private Address convertToAddr(ByteBlockInfo info) { - return ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()).getAddress( - info.getBlock(), info.getOffset()); - } - - private FieldLocation getFieldLocation(Address addr) { - ByteViewerComponent c = panel.getCurrentComponent(); - ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); - return c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); - } - - private void showComponent(final boolean visible) throws Exception { - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - tool.showComponentProvider(plugin.getProvider(), visible); - } - }); + private void showComponent(boolean visible) throws Exception { + SwingUtilities.invokeAndWait(() -> tool.showComponentProvider(provider, visible)); } } diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginCharFormatTest.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginCharFormatTest.java new file mode 100644 index 0000000000..9ad131103e --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginCharFormatTest.java @@ -0,0 +1,162 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.byteviewer; + +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.junit.Test; + +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.plugin.core.navigation.NavigationHistoryPlugin; +import ghidra.app.plugin.core.navigation.NextPrevAddressPlugin; +import ghidra.framework.plugintool.Plugin; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.listing.Program; +import ghidra.util.charset.CharsetInfoManager; + +/** + * Test for "chars" format + */ +public class ByteViewerPluginCharFormatTest extends AbstractByteViewerPluginTest { + + @Override + protected List> getDefaultPlugins() { + return List.of(NavigationHistoryPlugin.class, NextPrevAddressPlugin.class, + CodeBrowserPlugin.class); + } + + @Override + protected Program buildProgram() throws Exception { + ProgramBuilder builder = new ProgramBuilder("notepad", ProgramBuilder._TOY); + builder.createMemory("test2", "0x1001000", 0x2000); + builder.setBytes("0x1001000", "abc".getBytes(StandardCharsets.US_ASCII)); + builder.setBytes("0x1001100", "\u6211\u7684\u6c23\u588a\u8239\u88dd\u6eff\u4e86\u9c3b\u9b5a" + .getBytes(StandardCharsets.UTF_16BE)); + Program p = builder.getProgram(); + p.clearUndo(); + return p; + } + + @Test + public void testASCII() throws Exception { + loadViews("Chars"); + + ByteViewerComponent c = setView("Chars"); + goTo(addr(0x1001000)); + + assertEquals("a", getField(c, addr(0x1001000)).getText()); + assertEquals("b", getField(c, addr(0x1001001)).getText()); + assertEquals("c", getField(c, addr(0x1001002)).getText()); + assertEquals(".", getField(c, addr(0x1001003)).getText()); + } + + @Test + public void testIBM037() throws Exception { + // IBM code page 37 (EBCDIC, completely unrelated to ASCII) + loadViews("Chars"); + provider.setCharsetInfo(CharsetInfoManager.getInstance().get("IBM037")); + + ByteViewerComponent c = setView("Chars"); + goTo(addr(0x1001000)); + + // instead of 'a', 'b', 'c', we get '/', 'A-accent', 'A-otheraccent' + assertEquals("/", getField(c, addr(0x1001000)).getText()); + assertEquals("\u00c2", getField(c, addr(0x1001001)).getText()); + assertEquals("\u00c4", getField(c, addr(0x1001002)).getText()); + assertEquals(".", getField(c, addr(0x1001003)).getText()); + } + + @Test + public void testUTF16_alignment_off() throws Exception { + ByteViewerConfigOptions configOptions = provider.getConfigOptions().clone(); + configOptions.setUseCharAlignment(false); + provider.updateConfigOptions(configOptions, null); + + loadViews("Chars"); + + provider.setCharsetInfo(CharsetInfoManager.getInstance().get("UTF-16")); + + ByteViewerComponent c = setView("Chars"); + goTo(addr(0x1001100)); + + // overlapping UTF-16 chars [62 11] [11 76] + assertEquals("\u6211", getField(c, addr(0x1001100)).getText()); + assertEquals("\u1176", getField(c, addr(0x1001101)).getText()); + + assertEquals(16, c.getNumberOfFields()); // should have 16 (overlapping) chars per row + } + + @Test + public void testUTF16_alignment_on() throws Exception { + ByteViewerConfigOptions configOptions = provider.getConfigOptions().clone(); + configOptions.setUseCharAlignment(true); + provider.updateConfigOptions(configOptions, null); + + loadViews("Chars"); + + provider.setCharsetInfo(CharsetInfoManager.getInstance().get("UTF-16")); + + ByteViewerComponent c = setView("Chars"); + goTo(addr(0x1001100)); + + assertEquals("\u6211", getField(c, addr(0x1001100)).getText()); + assertEquals("\u6211", getField(c, addr(0x1001101)).getText()); // still same value because addr is offcut + + assertEquals("\u7684", getField(c, addr(0x1001102)).getText()); + + assertEquals(16 / 2, c.getNumberOfFields()); // should only have 8 chars per row instead of 16 + } + + @Test + public void testUTF32_badval() throws Exception { + ByteViewerConfigOptions configOptions = provider.getConfigOptions().clone(); + configOptions.setUseCharAlignment(true); + provider.updateConfigOptions(configOptions, null); + + loadViews("Chars"); + + provider.setCharsetInfo(CharsetInfoManager.getInstance().get("UTF-32")); + + ByteViewerComponent c = setView("Chars"); + goTo(addr(0x1001000)); + + assertEquals(".", getField(c, addr(0x1001100)).getText()); + } + + @Test + public void testWideChars() throws Exception { + loadViews("Chars"); + + provider.setCompactChars(true); + provider.setCharsetInfo(CharsetInfoManager.getInstance().get("UTF-16")); + + ByteViewerComponent c = setView("Chars"); + goTo(addr(0x1001100)); + + ByteField field = getField(c, addr(0x1001100)); + int compactWidth = field.getWidth(); + + provider.setCompactChars(false); + + field = getField(c, addr(0x1001100)); + int wideWidth = field.getWidth(); + + assertTrue(wideWidth == compactWidth * 2); // we know wide mode is 2x compact mode + } +} diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginFormatsTest.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginFormatsTest.java index fb37299228..bfde9d3462 100644 --- a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginFormatsTest.java +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPluginFormatsTest.java @@ -17,80 +17,50 @@ package ghidra.app.plugin.core.byteviewer; import static org.junit.Assert.*; -import java.awt.*; +import java.awt.Color; import java.awt.event.KeyEvent; import java.math.BigInteger; import java.util.Date; -import java.util.Map; +import java.util.List; -import javax.swing.*; +import javax.swing.JScrollBar; -import org.junit.*; +import org.junit.Test; import docking.DefaultActionContext; -import docking.action.DockingActionIf; import docking.action.ToggleDockingAction; import docking.widgets.fieldpanel.support.FieldLocation; import docking.widgets.indexedscrollpane.IndexedScrollPane; -import generic.test.TestUtils; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; import ghidra.app.plugin.core.format.*; import ghidra.app.plugin.core.navigation.NavigationHistoryPlugin; import ghidra.app.plugin.core.navigation.NextPrevAddressPlugin; import ghidra.app.services.GoToService; import ghidra.app.services.ProgramManager; -import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.Plugin; import ghidra.program.database.ProgramBuilder; import ghidra.program.database.ProgramDB; import ghidra.program.model.address.*; import ghidra.program.model.data.ByteDataType; import ghidra.program.model.data.StructureDataType; -import ghidra.program.model.listing.Listing; import ghidra.program.model.listing.Program; -import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.util.ProgramLocation; -import ghidra.test.AbstractGhidraHeadedIntegrationTest; -import ghidra.test.TestEnv; /** * Test for byte viewer formats. */ -public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegrationTest { +public class ByteViewerPluginFormatsTest extends AbstractByteViewerPluginTest { - private TestEnv env; - private PluginTool tool; - private Program program; - private Memory memory; - private Listing listing; - private ByteViewerPlugin plugin; - private ByteViewerPanel panel; - private ByteViewerComponentProvider provider; - @Before - public void setUp() throws Exception { - env = new TestEnv(); - tool = env.getTool(); - tool.addPlugin(NavigationHistoryPlugin.class.getName()); - tool.addPlugin(NextPrevAddressPlugin.class.getName()); - tool.addPlugin(CodeBrowserPlugin.class.getName()); - tool.addPlugin(ByteViewerPlugin.class.getName()); - - plugin = env.getPlugin(ByteViewerPlugin.class); - provider = - (ByteViewerComponentProvider) TestUtils.getInstanceField("connectedProvider", plugin); - tool.showComponentProvider(provider, true); - - program = buildNotepad(); - memory = program.getMemory(); - listing = program.getListing(); - ProgramManager pm = tool.getService(ProgramManager.class); - pm.openProgram(program.getDomainFile()); - panel = provider.getByteViewerPanel(); - waitForSwing(); + @Override + protected List> getDefaultPlugins() { + return List.of(NavigationHistoryPlugin.class, NextPrevAddressPlugin.class, + CodeBrowserPlugin.class); } - private Program buildNotepad() throws Exception { + @Override + protected Program buildProgram() throws Exception { ProgramBuilder builder = new ProgramBuilder("notepad", ProgramBuilder._TOY); builder.createMemory("test2", "0x1001000", 0x2000); Program p = builder.getProgram(); @@ -105,11 +75,6 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration return builder.getProgram(); } - @After - public void tearDown() throws Exception { - env.release(program); - env.dispose(); - } @Test public void testInsertionFieldHex() throws Exception { @@ -118,7 +83,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration // move the cursor to the right; the insertion field should not update runSwing(() -> { - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + FieldLocation loc = getFieldLocation(addr(0x01001000)); ByteViewerComponent c = panel.getCurrentComponent(); c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); c.cursorRight(); @@ -135,7 +100,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration // TestUtils.getInstanceField("updateManager", provider); // waitForSwingUpdateManager( updateManager ); waitForSwing(); - assertEquals(getAddr(0x01001001).toString(), findLabelStr(panel, "Insertion")); + assertEquals(addr(0x01001001).toString(), findLabelStr(panel, "Insertion")); // move the cursor to the left; the insertion field should have // been decremented by one @@ -143,7 +108,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration ByteViewerComponent c = panel.getCurrentComponent(); c.cursorLeft(); }); - assertEquals(getAddr(0x01001000).toString(), findLabelStr(panel, "Insertion")); + assertEquals(addr(0x01001000).toString(), findLabelStr(panel, "Insertion")); // move the cursor to the left; the insertion field should not change runSwing(() -> { @@ -164,7 +129,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration // move the cursor to the right two times; the insertion field should not update runSwing(() -> { - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + FieldLocation loc = getFieldLocation(addr(0x01001000)); ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); currentComponent.cursorRight(); @@ -177,7 +142,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.cursorRight(); }); - assertEquals(getAddr(0x01001001).toString(), findLabelStr(panel, "Insertion")); + assertEquals(addr(0x01001001).toString(), findLabelStr(panel, "Insertion")); // move the cursor to the left; the insertion field should have // been decremented by one @@ -185,7 +150,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.cursorLeft(); }); - assertEquals(getAddr(0x01001000).toString(), findLabelStr(panel, "Insertion")); + assertEquals(addr(0x01001000).toString(), findLabelStr(panel, "Insertion")); // move the cursor to the left two times; the insertion field should not change runSwing(() -> { @@ -200,26 +165,26 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration public void testInsertionFieldAscii() throws Exception { env.showTool(); addViews(); - ByteViewerComponent c = findComponent(panel, "Ascii"); + ByteViewerComponent c = findComponent(panel, "Chars"); panel.setCurrentView(c); String insertionStr = findLabelStr(panel, "Insertion"); // move the cursor to the right; the insertion field should update by one runSwing(() -> { - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + FieldLocation loc = getFieldLocation(addr(0x01001000)); ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); currentComponent.cursorRight(); }); - assertEquals(getAddr(0x01001001).toString(), findLabelStr(panel, "Insertion")); + assertEquals(addr(0x01001001).toString(), findLabelStr(panel, "Insertion")); // move the cursor to the right; the insertion field should update by one runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.cursorRight(); }); - assertEquals(getAddr(0x01001002).toString(), findLabelStr(panel, "Insertion")); + assertEquals(addr(0x01001002).toString(), findLabelStr(panel, "Insertion")); // move the cursor to the left; the insertion field should have // been decremented by one @@ -227,7 +192,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.cursorLeft(); }); - assertEquals(getAddr(0x01001001).toString(), findLabelStr(panel, "Insertion")); + assertEquals(addr(0x01001001).toString(), findLabelStr(panel, "Insertion")); // move the cursor to the left; the insertion field should update runSwing(() -> { @@ -248,7 +213,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration assertEquals(8, c.getNumberOfFields()); assertEquals(2, c.getDataModel().getUnitByteSize()); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -264,12 +229,11 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration env.showTool(); addViews(); - final ByteViewerComponent c = findComponent(panel, "Ascii"); + final ByteViewerComponent c = findComponent(panel, "Chars"); panel.setCurrentView(c); - final ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + ToggleDockingAction action = provider.getEditModeAction(); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -287,7 +251,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration runSwing(() -> { ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(getAddr(0x01001000)); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr(0x01001000)); FieldLocation l = hexComp.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); hexComp.setCursorPosition(l.getIndex(), l.getFieldNum(), 0, 0); }); @@ -307,7 +271,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration assertEquals(4, c.getNumberOfFields()); assertEquals(4, c.getDataModel().getUnitByteSize()); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -323,12 +287,11 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration env.showTool(); addViews(); - final ByteViewerComponent c = findComponent(panel, "Ascii"); + final ByteViewerComponent c = findComponent(panel, "Chars"); panel.setCurrentView(c); - final ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + ToggleDockingAction action = provider.getEditModeAction(); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -346,7 +309,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration runSwing(() -> { ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(getAddr(0x01001000)); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr(0x01001000)); FieldLocation l = hexComp.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); hexComp.setCursorPosition(l.getIndex(), l.getFieldNum(), 0, 0); }); @@ -366,7 +329,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration assertEquals(2, c.getNumberOfFields()); assertEquals(8, c.getDataModel().getUnitByteSize()); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -382,12 +345,11 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration env.showTool(); addViews(); - final ByteViewerComponent c = findComponent(panel, "Ascii"); + final ByteViewerComponent c = findComponent(panel, "Chars"); panel.setCurrentView(c); - final ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + ToggleDockingAction action = provider.getEditModeAction(); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -405,7 +367,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration runSwing(() -> { ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(getAddr(0x01001000)); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr(0x01001000)); FieldLocation l = hexComp.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); hexComp.setCursorPosition(l.getIndex(), l.getFieldNum(), 0, 0); }); @@ -425,7 +387,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration assertEquals(1, c.getNumberOfFields()); assertEquals(16, c.getDataModel().getUnitByteSize()); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -441,12 +403,11 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration env.showTool(); addViews(); - final ByteViewerComponent c = findComponent(panel, "Ascii"); + final ByteViewerComponent c = findComponent(panel, "Chars"); panel.setCurrentView(c); - final ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + ToggleDockingAction action = provider.getEditModeAction(); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -464,7 +425,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration runSwing(() -> { ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(getAddr(0x01001000)); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr(0x01001000)); FieldLocation l = hexComp.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); hexComp.setCursorPosition(l.getIndex(), l.getFieldNum(), 0, 0); }); @@ -479,7 +440,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration env.showTool(); runSwing(() -> { - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + FieldLocation loc = getFieldLocation(addr(0x01001000)); ByteViewerComponent c = panel.getCurrentComponent(); c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); }); @@ -506,13 +467,13 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration ByteViewerComponent currentComponent = panel.getCurrentComponent(); ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(getAddr(0x01001003)); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr(0x01001003)); currentComponent.setViewerCursorLocation(bbInfo.getBlock(), bbInfo.getOffset(), bbInfo.getColumn()); }); waitForSwing(); - FieldLocation loc = getFieldLocation(getAddr(0x01001003)); + FieldLocation loc = getFieldLocation(addr(0x01001003)); assertEquals(11, c.getCurrentField().getNumCols(loc.getRow())); // verify that no edits can be done in Integer format since it @@ -528,7 +489,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration env.showTool(); runSwing(() -> { - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + FieldLocation loc = getFieldLocation(addr(0x01001000)); ByteViewerComponent c = panel.getCurrentComponent(); c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); }); @@ -555,13 +516,13 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration ByteViewerComponent currentComponent = panel.getCurrentComponent(); ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(getAddr(0x01001003)); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr(0x01001003)); currentComponent.setViewerCursorLocation(bbInfo.getBlock(), bbInfo.getOffset(), bbInfo.getColumn()); }); waitForSwing(); - FieldLocation loc = getFieldLocation(getAddr(0x01001003)); + FieldLocation loc = getFieldLocation(addr(0x01001003)); assertEquals(3, c.getCurrentField().getNumCols(loc.getRow())); // verify that no edits can be done in Integer format since it @@ -577,19 +538,18 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration // verify that changed bytes are rendered in red in the Integer view env.showTool(); ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - setViewSelected(dialog, "Ascii", true); + setViewSelected(dialog, "Chars", true); setViewSelected(dialog, "Octal", true); setViewSelected(dialog, "Hex Integer", true); setViewSelected(dialog, "Integer", true); pressButtonByText(dialog.getComponent(), "OK"); waitForSwing(); - final ByteViewerComponent c = findComponent(panel, "Ascii"); + final ByteViewerComponent c = findComponent(panel, "Chars"); panel.setCurrentView(c); - final ToggleDockingAction action = - (ToggleDockingAction) getAction(plugin, "Enable/Disable Byteviewer Editing"); - final FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + ToggleDockingAction action = provider.getEditModeAction(); + final FieldLocation loc = getFieldLocation(addr(0x01001000)); runSwing(() -> { ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -608,7 +568,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration runSwing(() -> { ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(getAddr(0x01001000)); + ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr(0x01001000)); FieldLocation l = intComp.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); intComp.setCursorPosition(l.getIndex(), l.getFieldNum(), 0, 0); }); @@ -622,7 +582,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration env.showTool(); runSwing(() -> { - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); + FieldLocation loc = getFieldLocation(addr(0x01001000)); ByteViewerComponent c = panel.getCurrentComponent(); c.setCursorPosition(loc.getIndex(), loc.getFieldNum(), loc.getRow(), loc.getCol()); }); @@ -633,10 +593,10 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration final ByteViewerComponent c = findComponent(panel, "Integer"); panel.setCurrentView(c); - int v = program.getMemory().getInt(getAddr(0x01001000)); + int v = program.getMemory().getInt(addr(0x01001000)); runSwing(() -> { - FieldLocation loc = getFieldLocation(getAddr(0x01001000)); - DockingActionIf action = getAction(plugin, "Enable/Disable Byteviewer Editing"); + FieldLocation loc = getFieldLocation(addr(0x01001000)); + ToggleDockingAction action = provider.getEditModeAction(); ByteViewerComponent currentComponent = panel.getCurrentComponent(); currentComponent.setCursorPosition(loc.getIndex(), loc.getFieldNum(), 0, 0); @@ -649,7 +609,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration }); program.flushEvents(); // verify that no changes were made - assertEquals(v, program.getMemory().getInt(getAddr(0x01001000))); + assertEquals(v, program.getMemory().getInt(addr(0x01001000))); } @Test @@ -782,12 +742,12 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration panel.setCurrentView(c); GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, getAddr(01001530))); + goToService.goTo(new ProgramLocation(program, addr(01001530))); waitForSwing(); int transactionID = program.startTransaction("test"); - Address addr = getAddr(0x01001530); + Address addr = addr(0x01001530); for (int i = 0; i < 5; i++) { listing.createData(addr, new ByteDataType(), 1); addr = addr.add(1); @@ -797,7 +757,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration program.flushEvents(); waitForSwing(); - addr = getAddr(0x01001530); + addr = addr(0x01001530); for (int i = 0; i < 5; i++) { FieldLocation loc = getFieldLocation(addr); ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); @@ -821,12 +781,12 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration panel.setCurrentView(c); GoToService goToService = tool.getService(GoToService.class); - goToService.goTo(new ProgramLocation(program, getAddr(01001530))); + goToService.goTo(new ProgramLocation(program, addr(01001530))); waitForSwing(); int transactionID = program.startTransaction("test"); - Address addr = getAddr(0x01001530); + Address addr = addr(0x01001530); StructureDataType dt = new StructureDataType("test", 0); for (int i = 0; i < 7; i++) { dt.add(new ByteDataType()); @@ -837,7 +797,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration program.flushEvents(); waitForSwing(); - addr = getAddr(0x01001530); + addr = addr(0x01001530); for (int i = 0; i < dt.getLength(); i++) { FieldLocation loc = getFieldLocation(addr); ByteField field = c.getField(loc.getIndex(), loc.getFieldNum()); @@ -910,100 +870,7 @@ public class ByteViewerPluginFormatsTest extends AbstractGhidraHeadedIntegration return null; } - private Address getAddr(long offset) { - return getAddr(program, offset); - } - - private Address getAddr(Program p, long offset) { - return p.getMinAddress().getNewAddress(offset); - } - - private FieldLocation getFieldLocation(Address addr) { - ByteViewerComponent c = panel.getCurrentComponent(); - ProgramByteBlockSet blockset = (ProgramByteBlockSet) plugin.getProvider().getByteBlockSet(); - ByteBlockInfo bbInfo = blockset.getByteBlockInfo(addr); - return c.getFieldLocation(bbInfo.getBlock(), bbInfo.getOffset()); - - } - - private String findLabelStr(Container container, String name) { - Component[] c = container.getComponents(); - for (Component element : c) { - if (element instanceof JLabel) { - if (name.equals(element.getName())) { - return ((JLabel) element).getText(); - } - } - if (element instanceof Container) { - String str = findLabelStr((Container) element, name); - if (str != null) { - return str; - } - } - } - return null; - } - - private ByteViewerComponent findComponent(Container container, String name) { - Component[] c = container.getComponents(); - for (Component element : c) { - if (element instanceof ByteViewerComponent) { - if (((ByteViewerComponent) element).getDataModel().getName().equals(name)) { - return (ByteViewerComponent) element; - } - } - else if (element instanceof Container) { - ByteViewerComponent bvc = findComponent((Container) element, name); - if (bvc != null) { - return bvc; - } - } - } - return null; - } - - private Container findContainer(Container parent, Class theClass) { - Component[] c = parent.getComponents(); - for (Component element : c) { - if (element.getClass() == theClass) { - return (Container) element; - } - if (element instanceof Container) { - Container container = findContainer((Container) element, theClass); - if (container != null) { - return container; - } - } - } - return null; - } - private void addViews() throws Exception { - ByteViewerOptionsDialog dialog = launchByteViewerOptions(); - setViewSelected(dialog, "Ascii", true); - setViewSelected(dialog, "Octal", true); - setViewSelected(dialog, "Hex Short", true); - setViewSelected(dialog, "Hex Integer", true); - setViewSelected(dialog, "Hex Long", true); - setViewSelected(dialog, "Hex Long Long", true); - pressButtonByText(dialog.getComponent(), "OK"); - waitForSwing(); - } - - private void setViewSelected(ByteViewerOptionsDialog dialog, String viewName, - boolean selected) { - Map checkboxMap = (Map) getInstanceField("checkboxMap", dialog); - JCheckBox checkbox = (JCheckBox) checkboxMap.get(viewName); - checkbox.setSelected(selected); - } - - private ByteViewerOptionsDialog launchByteViewerOptions() { - final DockingActionIf action = getAction(plugin, "Byte Viewer Options"); - assertTrue(action.isEnabled()); - - runSwing(() -> action.actionPerformed(new DefaultActionContext()), false); - waitForSwing(); - ByteViewerOptionsDialog d = waitForDialogComponent(ByteViewerOptionsDialog.class); - return d; + loadViews("Chars", "Octal", "Hex Short", "Hex Integer", "Hex Long", "Hex Long Long"); } } diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbReaderOptions.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbReaderOptions.java index bb7c083b9b..a599b34cae 100644 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbReaderOptions.java +++ b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbReaderOptions.java @@ -19,8 +19,8 @@ import java.nio.charset.Charset; import java.util.List; import ghidra.framework.options.Options; -import ghidra.program.model.data.CharsetInfo; import ghidra.util.HelpLocation; +import ghidra.util.charset.CharsetInfoManager; /** * Options used while reading a PDB ({@link AbstractPdb}) that control various aspects. These @@ -39,7 +39,7 @@ public class PdbReaderOptions extends Exception { private static final String OPTION_DESCRIPTION_ONE_BYTE_CHARSET_NAME = "Charset used for processing of one-byte (or multi) encoded Strings: " + PdbReaderOptions.getOneByteCharsetNames(); - private static final String DEFAULT_ONE_BYTE_CHARSET_NAME = CharsetInfo.UTF8; + private static final String DEFAULT_ONE_BYTE_CHARSET_NAME = CharsetInfoManager.UTF8; private String oneByteCharsetName; // Sets the wchar_t Charset to be used for PDB processing. @@ -49,14 +49,14 @@ public class PdbReaderOptions extends Exception { private static final String OPTION_DESCRIPTION_WCHAR_CHARSET_NAME = "Charset used for processing of wchar_t encoded Strings: " + PdbReaderOptions.getTwoByteCharsetNames(); - private static final String DEFAULT_TWO_BYTE_CHARSET_NAME = CharsetInfo.UTF16; + private static final String DEFAULT_TWO_BYTE_CHARSET_NAME = CharsetInfoManager.UTF16; private String wideCharCharsetName; //============================================================================================== private static List oneByteCharsetNames = - CharsetInfo.getInstance().getCharsetNamesWithCharSize(1); + CharsetInfoManager.getInstance().getCharsetNamesWithCharSize(1); private static List twoByteCharsetNames = - CharsetInfo.getInstance().getCharsetNamesWithCharSize(2); + CharsetInfoManager.getInstance().getCharsetNamesWithCharSize(2); private Charset oneByteCharset; private Charset wideCharset; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java index c08e4aaaa4..cdbd8e15ac 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/spinner/IntegerSpinner.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,12 +15,11 @@ */ package docking.widgets.spinner; -import java.util.ArrayList; -import java.util.List; - import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; @@ -29,12 +28,13 @@ import javax.swing.event.*; import docking.widgets.textfield.IntegerTextField; /** - * Creates a component for editing Integer values using an {@link IntegerTextField} and a {@link JSpinner}. + * Creates a component for editing Integer values using an {@link IntegerTextField} and a + * {@link JSpinner}. */ public class IntegerSpinner { - private final JSpinner spinner; - private final IntegerTextField integerTextField; + protected final JSpinner spinner; + protected final IntegerTextField integerTextField; private List changeListeners = new ArrayList<>(); @@ -51,6 +51,7 @@ public class IntegerSpinner { * Creates a new IntegerSpinner using the given spinner model. * * @param spinnerModel the spinner model to use in the JSpinner. + * @param columns number of columns to display in in the JTextField */ public IntegerSpinner(SpinnerNumberModel spinnerModel, int columns) { @@ -66,6 +67,14 @@ public class IntegerSpinner { spinner.setEditor(integerTextField.getComponent()); spinnerModel.addChangeListener(e -> { + Number newMaxNum = (Number) spinnerModel.getMaximum(); + BigInteger newMax = + newMaxNum != null ? BigInteger.valueOf(newMaxNum.longValue()) : null; + BigInteger oldMax = integerTextField.getMaxValue(); + if (newMax != null && !newMax.equals(oldMax) || (newMax == null && oldMax != null)) { + integerTextField.setMaxValue(newMax); + } + Number newVal = (Number) spinnerModel.getValue(); integerTextField.setValue(newVal.longValue()); }); @@ -120,7 +129,6 @@ public class IntegerSpinner { } spinnerModel.setValue(value.longValue()); }); - } /** diff --git a/Ghidra/Framework/SoftwareModeling/certification.manifest b/Ghidra/Framework/SoftwareModeling/certification.manifest index a0ae6dbbce..1a9c839cf4 100644 --- a/Ghidra/Framework/SoftwareModeling/certification.manifest +++ b/Ghidra/Framework/SoftwareModeling/certification.manifest @@ -7,7 +7,7 @@ Module.manifest||GHIDRA||||END| README.md||GHIDRA||||END| data/ExtensionPoint.manifest||GHIDRA||||END| -data/charset_info.xml||GHIDRA||||END| +data/charset_info.json||GHIDRA||||END| data/languages/compiler_spec.rxg||GHIDRA||||END| data/languages/format_opinions.rxg||GHIDRA||||END| data/languages/language_common.rxg||GHIDRA||||END| diff --git a/Ghidra/Framework/SoftwareModeling/data/charset_info.json b/Ghidra/Framework/SoftwareModeling/data/charset_info.json new file mode 100644 index 0000000000..3fec89c4f9 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/data/charset_info.json @@ -0,0 +1,3213 @@ +{ + "comments": [ + "Information about character encodings used by Ghidra.", + "Generated on Thu Dec 18 21:18:49 UTC 2025 by CharsetInfoManagerTest.generateCharsetInfoFile" + ], + "charsets": [ + { + "name": "Big5", + "comment": "A traditional Chinese encoding used mainly in Taiwan and Hong Kong for Traditional Chinese characters. It supports a large repertoire but does not cover all Unicode characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 13830, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "Big5-HKSCS", + "comment": "An extension of Big5 used in Hong Kong that adds Cantonese-specific and government-required characters. It improves compatibility with local applications and legacy data.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 18578, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "Big5", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-Big5-HKSCS-2001", + "comment": "An older version of the Hong Kong Supplementary Character Set based on Big5. It includes Cantonese-specific and government-required characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 18384, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "Big5", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-Big5-Solaris", + "comment": "A Solaris-specific variant of Big5. It includes vendor extensions used on Sun systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 13837, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "Big5", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "CESU-8", + "comment": "A variant of UTF-8 designed for internal processing that preserves UTF-16 surrogate pairs. It is not intended for data interchange and is rarely used outside specific systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 6, + "alignment": 1, + "codePointCount": 149249, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "ARMENIAN", + "HEBREW", + "ARABIC", + "SYRIAC", + "THAANA", + "DEVANAGARI", + "BENGALI", + "GURMUKHI", + "GUJARATI", + "ORIYA", + "TAMIL", + "TELUGU", + "KANNADA", + "MALAYALAM", + "SINHALA", + "THAI", + "LAO", + "TIBETAN", + "MYANMAR", + "GEORGIAN", + "HANGUL", + "ETHIOPIC", + "CHEROKEE", + "CANADIAN_ABORIGINAL", + "OGHAM", + "RUNIC", + "KHMER", + "MONGOLIAN", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN", + "YI", + "OLD_ITALIC", + "GOTHIC", + "DESERET", + "INHERITED", + "TAGALOG", + "HANUNOO", + "BUHID", + "TAGBANWA", + "LIMBU", + "TAI_LE", + "LINEAR_B", + "UGARITIC", + "SHAVIAN", + "OSMANYA", + "CYPRIOT", + "BRAILLE", + "BUGINESE", + "COPTIC", + "NEW_TAI_LUE", + "GLAGOLITIC", + "TIFINAGH", + "SYLOTI_NAGRI", + "OLD_PERSIAN", + "KHAROSHTHI", + "BALINESE", + "CUNEIFORM", + "PHOENICIAN", + "PHAGS_PA", + "NKO", + "SUNDANESE", + "BATAK", + "LEPCHA", + "OL_CHIKI", + "VAI", + "SAURASHTRA", + "KAYAH_LI", + "REJANG", + "LYCIAN", + "CARIAN", + "LYDIAN", + "CHAM", + "TAI_THAM", + "TAI_VIET", + "AVESTAN", + "EGYPTIAN_HIEROGLYPHS", + "SAMARITAN", + "MANDAIC", + "LISU", + "BAMUM", + "JAVANESE", + "MEETEI_MAYEK", + "IMPERIAL_ARAMAIC", + "OLD_SOUTH_ARABIAN", + "INSCRIPTIONAL_PARTHIAN", + "INSCRIPTIONAL_PAHLAVI", + "OLD_TURKIC", + "BRAHMI", + "KAITHI", + "MEROITIC_HIEROGLYPHS", + "MEROITIC_CURSIVE", + "SORA_SOMPENG", + "CHAKMA", + "SHARADA", + "TAKRI", + "MIAO", + "CAUCASIAN_ALBANIAN", + "BASSA_VAH", + "DUPLOYAN", + "ELBASAN", + "GRANTHA", + "PAHAWH_HMONG", + "KHOJKI", + "LINEAR_A", + "MAHAJANI", + "MANICHAEAN", + "MENDE_KIKAKUI", + "MODI", + "MRO", + "OLD_NORTH_ARABIAN", + "NABATAEAN", + "PALMYRENE", + "PAU_CIN_HAU", + "OLD_PERMIC", + "PSALTER_PAHLAVI", + "SIDDHAM", + "KHUDAWADI", + "TIRHUTA", + "WARANG_CITI", + "AHOM", + "ANATOLIAN_HIEROGLYPHS", + "HATRAN", + "MULTANI", + "OLD_HUNGARIAN", + "SIGNWRITING", + "ADLAM", + "BHAIKSUKI", + "MARCHEN", + "NEWA", + "OSAGE", + "TANGUT", + "MASARAM_GONDI", + "NUSHU", + "SOYOMBO", + "ZANABAZAR_SQUARE", + "HANIFI_ROHINGYA", + "OLD_SOGDIAN", + "SOGDIAN", + "DOGRA", + "GUNJALA_GONDI", + "MAKASAR", + "MEDEFAIDRIN", + "ELYMAIC", + "NANDINAGARI", + "NYIAKENG_PUACHUE_HMONG", + "WANCHO", + "YEZIDI", + "CHORASMIAN", + "DIVES_AKURU", + "KHITAN_SMALL_SCRIPT", + "VITHKUQI", + "OLD_UYGHUR", + "CYPRO_MINOAN", + "TANGSA", + "TOTO", + "KAWI", + "NAG_MUNDARI" + ], + "contains": [ + "Big5-HKSCS", + "x-ISO-2022-CN-CNS", + "KOI8-R", + "JIS_X0212-1990", + "x-Johab", + "windows-31j", + "GB2312", + "Shift_JIS", + "x-ISO-2022-CN-GB", + "windows-1251", + "UTF-8", + "ISO-8859-13", + "windows-1253", + "windows-1252", + "windows-1255", + "windows-1254", + "windows-1257", + "ISO-8859-16", + "windows-1256", + "ISO-8859-15", + "US-ASCII", + "windows-1258", + "x-UTF-16LE-BOM", + "UTF-16LE", + "x-MS950-HKSCS", + "Big5", + "x-windows-950", + "EUC-JP", + "x-mswin-936", + "GB18030", + "x-euc-jp-linux", + "GBK", + "x-JIS0208", + "ISO-2022-KR", + "ISO-8859-4", + "x-windows-949", + "ISO-8859-3", + "TIS-620", + "ISO-8859-2", + "x-EUC-TW", + "ISO-8859-1", + "ISO-8859-8", + "ISO-8859-7", + "ISO-8859-6", + "ISO-8859-5", + "UTF-16", + "EUC-KR", + "ISO-8859-9", + "x-ISCII91", + "ISO-2022-JP", + "UTF-16BE", + "JIS_X0201" + ], + "canProduceError": true + }, + { + "name": "EUC-JP", + "comment": "A multibyte encoding for Japanese that includes ASCII, JIS X 0208, and half-width kana. It was widely used on Unix systems before UTF-8 became dominant.", + "minBytesPerChar": 1, + "maxBytesPerChar": 3, + "alignment": 1, + "codePointCount": 13138, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "x-JIS0208", + "JIS_X0201", + "JIS_X0212-1990", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-euc-jp-linux", + "comment": "A Linux-specific variant of EUC-JP. It includes additional mappings used by Japanese Linux distributions.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7071, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "JIS_X0201", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "EUC-KR", + "comment": "A Korean encoding based on KS X 1001 combined with ASCII. It supports modern Hangul syllables and was common on Unix platforms.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 8354, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-EUC-TW", + "comment": "A Taiwanese encoding supporting Traditional Chinese and additional plane mappings. It includes characters from CNS 11643.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 55567, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-eucJP-Open", + "comment": "An extended EUC-JP variant used in some open-source systems. It includes additional Kanji beyond standard EUC-JP.", + "minBytesPerChar": 1, + "maxBytesPerChar": 3, + "alignment": 1, + "codePointCount": 13304, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "EUC-JP", + "JIS_X0201", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "GB18030", + "comment": "China\u0027s mandatory national encoding standard that covers all Unicode characters using 1, 2, or 4 bytes. It supersedes GBK and GB2312 and ensures compatibility with rare and historical characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 149249, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "ARMENIAN", + "HEBREW", + "ARABIC", + "SYRIAC", + "THAANA", + "DEVANAGARI", + "BENGALI", + "GURMUKHI", + "GUJARATI", + "ORIYA", + "TAMIL", + "TELUGU", + "KANNADA", + "MALAYALAM", + "SINHALA", + "THAI", + "LAO", + "TIBETAN", + "MYANMAR", + "GEORGIAN", + "HANGUL", + "ETHIOPIC", + "CHEROKEE", + "CANADIAN_ABORIGINAL", + "OGHAM", + "RUNIC", + "KHMER", + "MONGOLIAN", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN", + "YI", + "OLD_ITALIC", + "GOTHIC", + "DESERET", + "INHERITED", + "TAGALOG", + "HANUNOO", + "BUHID", + "TAGBANWA", + "LIMBU", + "TAI_LE", + "LINEAR_B", + "UGARITIC", + "SHAVIAN", + "OSMANYA", + "CYPRIOT", + "BRAILLE", + "BUGINESE", + "COPTIC", + "NEW_TAI_LUE", + "GLAGOLITIC", + "TIFINAGH", + "SYLOTI_NAGRI", + "OLD_PERSIAN", + "KHAROSHTHI", + "BALINESE", + "CUNEIFORM", + "PHOENICIAN", + "PHAGS_PA", + "NKO", + "SUNDANESE", + "BATAK", + "LEPCHA", + "OL_CHIKI", + "VAI", + "SAURASHTRA", + "KAYAH_LI", + "REJANG", + "LYCIAN", + "CARIAN", + "LYDIAN", + "CHAM", + "TAI_THAM", + "TAI_VIET", + "AVESTAN", + "EGYPTIAN_HIEROGLYPHS", + "SAMARITAN", + "MANDAIC", + "LISU", + "BAMUM", + "JAVANESE", + "MEETEI_MAYEK", + "IMPERIAL_ARAMAIC", + "OLD_SOUTH_ARABIAN", + "INSCRIPTIONAL_PARTHIAN", + "INSCRIPTIONAL_PAHLAVI", + "OLD_TURKIC", + "BRAHMI", + "KAITHI", + "MEROITIC_HIEROGLYPHS", + "MEROITIC_CURSIVE", + "SORA_SOMPENG", + "CHAKMA", + "SHARADA", + "TAKRI", + "MIAO", + "CAUCASIAN_ALBANIAN", + "BASSA_VAH", + "DUPLOYAN", + "ELBASAN", + "GRANTHA", + "PAHAWH_HMONG", + "KHOJKI", + "LINEAR_A", + "MAHAJANI", + "MANICHAEAN", + "MENDE_KIKAKUI", + "MODI", + "MRO", + "OLD_NORTH_ARABIAN", + "NABATAEAN", + "PALMYRENE", + "PAU_CIN_HAU", + "OLD_PERMIC", + "PSALTER_PAHLAVI", + "SIDDHAM", + "KHUDAWADI", + "TIRHUTA", + "WARANG_CITI", + "AHOM", + "ANATOLIAN_HIEROGLYPHS", + "HATRAN", + "MULTANI", + "OLD_HUNGARIAN", + "SIGNWRITING", + "ADLAM", + "BHAIKSUKI", + "MARCHEN", + "NEWA", + "OSAGE", + "TANGUT", + "MASARAM_GONDI", + "NUSHU", + "SOYOMBO", + "ZANABAZAR_SQUARE", + "HANIFI_ROHINGYA", + "OLD_SOGDIAN", + "SOGDIAN", + "DOGRA", + "GUNJALA_GONDI", + "MAKASAR", + "MEDEFAIDRIN", + "ELYMAIC", + "NANDINAGARI", + "NYIAKENG_PUACHUE_HMONG", + "WANCHO", + "YEZIDI", + "CHORASMIAN", + "DIVES_AKURU", + "KHITAN_SMALL_SCRIPT", + "VITHKUQI", + "OLD_UYGHUR", + "CYPRO_MINOAN", + "TANGSA", + "TOTO", + "KAWI", + "NAG_MUNDARI" + ], + "contains": [ + "Big5-HKSCS", + "x-MS950-HKSCS", + "x-ISO-2022-CN-CNS", + "Big5", + "x-windows-950", + "EUC-JP", + "x-mswin-936", + "KOI8-R", + "GBK", + "x-Johab", + "windows-31j", + "GB2312", + "ISO-2022-KR", + "ISO-8859-4", + "x-windows-949", + "ISO-8859-3", + "TIS-620", + "ISO-8859-2", + "Shift_JIS", + "x-EUC-TW", + "ISO-8859-1", + "ISO-8859-8", + "ISO-8859-7", + "x-ISO-2022-CN-GB", + "ISO-8859-6", + "windows-1251", + "ISO-8859-5", + "UTF-16", + "UTF-8", + "ISO-8859-13", + "windows-1253", + "windows-1252", + "EUC-KR", + "windows-1255", + "ISO-8859-9", + "windows-1254", + "windows-1257", + "ISO-8859-16", + "windows-1256", + "ISO-8859-15", + "US-ASCII", + "windows-1258", + "UTF-16LE", + "x-ISCII91", + "ISO-2022-JP", + "UTF-16BE", + "JIS_X0201" + ], + "canProduceError": true + }, + { + "name": "GB2312", + "comment": "An early Simplified Chinese encoding standard covering the most common characters used in mainland China. It served as the foundation for GBK and GB18030.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7572, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "GBK", + "comment": "An extension of GB2312 that adds thousands of Simplified and Traditional Chinese characters. It is widely used in Windows systems and is backward-compatible with GB2312.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 21919, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "IBM-Thai", + "comment": "An IBM code page for Thai script based on TIS-620. It supports Thai characters and punctuation for legacy IBM systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 250, + "scripts": [ + "COMMON", + "LATIN", + "THAI" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM00858", + "comment": "A DOS-era IBM code page similar to CP850 but with the Euro symbol added. It supports Western European languages in legacy environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01140", + "comment": "An EBCDIC code page based on IBM037 with Euro support. It is used on IBM mainframes for Western European languages.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01141", + "comment": "An EBCDIC code page similar to IBM273 with Euro support. It supports German-language environments on IBM systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01142", + "comment": "An EBCDIC code page derived from IBM277 with Euro support. It is used for Danish and Norwegian text on IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01143", + "comment": "An EBCDIC code page based on IBM278 with Euro support. It is designed for Finnish and Swedish environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01144", + "comment": "An EBCDIC code page derived from IBM280 with Euro support. It supports Italian-language mainframe systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01145", + "comment": "An EBCDIC code page based on IBM284 with Euro support. It is used for Spanish-language environments on IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01146", + "comment": "An EBCDIC code page derived from IBM285 with Euro support. It supports UK English on IBM systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01147", + "comment": "An EBCDIC code page based on IBM297 with Euro support. It is used for French-language mainframe environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01148", + "comment": "An EBCDIC code page derived from IBM500 with Euro support. It supports multiple Western European languages on IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM01149", + "comment": "An EBCDIC code page based on IBM871 with Euro support. It is used for Icelandic-language environments on IBM systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM037", + "comment": "A core EBCDIC code page used in the United States and some Western countries. It supports basic Latin characters and control codes for IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1006", + "comment": "An IBM code page for Urdu written in Arabic script (IBM EBCDIC). It supports contextual shaping for proper display.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM1025", + "comment": "An IBM Cyrillic code page used in Eastern Europe (IBM EBCDIC). It supports Russian, Bulgarian, and Serbian Cyrillic.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM1026", + "comment": "An EBCDIC code page used for Turkish language support. It includes Latin characters with Turkish-specific letters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1046", + "comment": "An IBM Arabic code page supporting contextual shaping (IBM EBCDIC). It is used in Middle Eastern IBM environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 245, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM1047", + "comment": "An EBCDIC code page designed to better align with ASCII for easier interoperability. It is used in Unix-like environments on IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1097", + "comment": "An IBM Farsi code page (IBM EBCDIC). It supports Arabic-derived Persian script with contextual shaping.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 252, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC", + "INHERITED" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM1098", + "comment": "An IBM code page for Farsi with alternative mappings (IBM EBCDIC). It supports Persian letters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 249, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC", + "INHERITED" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM1112", + "comment": "An IBM code page for Estonian (IBM EBCDIC). It includes diacritics and letters used in Estonian orthography.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1122", + "comment": "An IBM code page for Latvian (IBM EBCDIC). It supports Latvian diacritics and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1123", + "comment": "An IBM code page for Ukrainian (IBM EBCDIC). It includes Ukrainian Cyrillic letters not found in KOI8-R.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1124", + "comment": "An IBM code page for Lithuanian (IBM EBCDIC). It supports Lithuanian diacritics and special characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1129", + "comment": "An IBM code page for Vietnamese (IBM EBCDIC). It uses combining diacritics to represent Vietnamese tones.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 349, + "scripts": [ + "COMMON", + "LATIN", + "INHERITED" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM1166", + "comment": "An IBM code page for Belarusian (IBM EBCDIC). It includes Belarusian Cyrillic letters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM1364", + "comment": "An IBM code page for Korean (IBM EBCDIC). It supports Hangul syllables and Hanja characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 17847, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM1381", + "comment": "An IBM code page for Simplified Chinese (IBM EBCDIC). It includes GB2312 characters and IBM extensions.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7609, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-IBM1383", + "comment": "An IBM code page for Traditional Chinese (IBM EBCDIC). It includes Big5 characters and IBM-specific additions.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7634, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "IBM273", + "comment": "An EBCDIC code page used for German-language systems. It includes German umlauts and standard Latin characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM277", + "comment": "An EBCDIC code page for Danish and Norwegian languages. It supports regional letters and punctuation used in Scandinavia.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM278", + "comment": "An EBCDIC code page used for Finnish and Swedish languages. It includes Nordic characters and standard Latin letters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM280", + "comment": "An EBCDIC code page for Italian-language environments. It supports accented Italian characters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM284", + "comment": "An EBCDIC code page used for Spanish-language systems. It includes Spanish accented letters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM285", + "comment": "An EBCDIC code page for UK English. It supports British punctuation and currency symbols.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM290", + "comment": "An EBCDIC code page for Japanese Katakana. It was used on IBM mainframes before multibyte encodings became common.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 321, + "scripts": [ + "COMMON", + "LATIN", + "KATAKANA" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM29626C", + "comment": "An IBM code page for Chinese used in AIX systems (IBM EBCDIC). It supports CNS 11643 planes and extended characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 3, + "alignment": 1, + "codePointCount": 7595, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "IBM297", + "comment": "An EBCDIC code page for French-language systems. It includes accented French characters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM300", + "comment": "A double-byte IBM code page for Japanese (IBM EBCDIC). It supports Kanji, Hiragana, Katakana, and symbols.", + "minBytesPerChar": 2, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7309, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM33722", + "comment": "An IBM code page for Korean used in AIX (IBM EBCDIC). It supports KS X 1001 and extended Hangul.", + "minBytesPerChar": 1, + "maxBytesPerChar": 3, + "alignment": 1, + "codePointCount": 7540, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM420", + "comment": "An EBCDIC code page for Arabic script. It supports contextual shaping required for Arabic text.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 427, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC", + "INHERITED" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM424", + "comment": "An EBCDIC code page for Hebrew script. It supports Hebrew letters and right-to-left text direction.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 217, + "scripts": [ + "COMMON", + "LATIN", + "HEBREW" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM437", + "comment": "The original IBM PC code page used in DOS. It includes box-drawing characters and basic Latin letters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM500", + "comment": "An EBCDIC code page supporting multiple Western European languages. It includes accented characters and punctuation for multilingual environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM737", + "comment": "A DOS code page for Greek. It includes Greek letters and basic punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM775", + "comment": "A code page used for Baltic languages such as Latvian and Lithuanian. It supports diacritics and regional characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM833", + "comment": "An IBM code page for Korean (IBM EBCDIC). It supports early Hangul encodings and Hanja.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 310, + "scripts": [ + "COMMON", + "LATIN", + "HANGUL" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM834", + "comment": "An IBM code page for Korean similar to x-IBM833 but with additional characters (IBM EBCDIC). It supports extended Hangul and Hanja.", + "minBytesPerChar": 2, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 8839, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM850", + "comment": "A DOS code page for Western European languages. It expands on CP437 by adding more accented characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM852", + "comment": "A DOS code page for Central and Eastern European languages. It supports diacritics used in Polish, Czech, Slovak, and related languages.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM855", + "comment": "A DOS code page for Cyrillic languages. It includes Russian, Bulgarian, and Serbian Cyrillic characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM856", + "comment": "A DOS code page for Hebrew. It supports Hebrew letters but lacks right-to-left control characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 214, + "scripts": [ + "COMMON", + "LATIN", + "HEBREW" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM857", + "comment": "A DOS code page for Turkish. It includes Turkish-specific letters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 252, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM860", + "comment": "A DOS code page for Portuguese as used in Portugal. It includes accented characters and symbols.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM861", + "comment": "A DOS code page for Icelandic. It includes Icelandic letters and diacritics.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM862", + "comment": "A DOS code page for Hebrew. It supports Hebrew letters and right-to-left text.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "HEBREW" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM863", + "comment": "A DOS code page for Canadian French. It includes accented French characters used in Canada.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM864", + "comment": "A DOS code page for Arabic. It supports Arabic letters and contextual forms.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 250, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "ARABIC", + "INHERITED" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM865", + "comment": "A DOS code page for Nordic languages. It includes characters used in Danish, Norwegian, and Icelandic.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM866", + "comment": "A DOS code page for Russian and other Cyrillic languages. It became widely used in Eastern Europe and the former Soviet Union.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM868", + "comment": "A DOS code page for Arabic used in some regions. It supports Arabic letters and basic punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 253, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM869", + "comment": "A DOS code page for Greek. It includes Greek letters and diacritics.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 246, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM870", + "comment": "An EBCDIC code page for multilingual Latin scripts including Eastern European languages. It supports accented characters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "IBM871", + "comment": "An EBCDIC code page for Icelandic. It includes Icelandic letters and special characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM874", + "comment": "A Windows and DOS code page for Thai. It supports Thai script without contextual shaping.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 219, + "scripts": [ + "COMMON", + "LATIN", + "THAI" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM875", + "comment": "An EBCDIC code page for Greek. It includes Greek letters and punctuation for IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 249, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "IBM918", + "comment": "An EBCDIC code page for Urdu. It supports Arabic-derived Urdu script with contextual shaping.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM921", + "comment": "A Baltic code page supporting Latvian, Lithuanian, and Estonian. It includes regional diacritics and symbols.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM922", + "comment": "A Baltic code page similar to x-IBM921 but with alternative mappings. It supports additional regional characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-IBM930", + "comment": "A Japanese EBCDIC mixed single-byte and double-byte code page. It supports Kanji and Katakana for IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 7541, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM933", + "comment": "A Korean EBCDIC mixed-byte code page. It supports Hangul and Hanja for IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 9047, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM935", + "comment": "A Simplified Chinese EBCDIC mixed-byte code page. It supports GB2312 characters for IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 7636, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM937", + "comment": "A Traditional Chinese EBCDIC mixed-byte code page. It supports Big5 characters for IBM mainframes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 14026, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM939", + "comment": "A Japanese EBCDIC mixed-byte code page similar to x-IBM930 but with extended characters. It supports Kanji and Katakana.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 7542, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM942", + "comment": "A Shift_JIS-based IBM code page for Japanese. It includes IBM extensions and vendor-specific characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7511, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM942C", + "comment": "A variant of x-IBM942 with corrected mappings. It improves compatibility with standard Shift_JIS.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7511, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-IBM943", + "comment": "An IBM code page combining Shift_JIS with additional Kanji. It supports a wide range of Japanese characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7560, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM943C", + "comment": "A corrected version of x-IBM943 with improved character mappings. It enhances compatibility with Windows-31J.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7562, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-IBM948", + "comment": "An IBM code page for Simplified Chinese based on GBK. It includes IBM extensions for additional characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 13996, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-IBM949", + "comment": "An IBM code page for Korean based on EUC-KR. It includes extended Hangul syllables.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 8965, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM949C", + "comment": "A corrected version of x-IBM949 with improved mappings. It enhances compatibility with Windows Korean encodings.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 8965, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-IBM950", + "comment": "An IBM code page for Traditional Chinese based on Big5. It includes IBM extensions for additional characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 13993, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-IBM964", + "comment": "An IBM code page for Traditional Chinese used in Taiwan. It supports CNS 11643 plane mappings.", + "minBytesPerChar": 1, + "maxBytesPerChar": 4, + "alignment": 1, + "codePointCount": 14023, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-IBM970", + "comment": "An IBM code page for Korean supporting Hangul and Hanja. It is used in IBM mainframe environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 8388, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-ISCII91", + "comment": "An Indian encoding standard supporting multiple Indic scripts through a shared code structure. It uses shift codes to switch between scripts.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 233, + "scripts": [ + "COMMON", + "LATIN", + "DEVANAGARI", + "INHERITED" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-ISO-2022-CN-CNS", + "comment": "A variant of ISO-2022-CN supporting CNS 11643 character planes. It enables Traditional Chinese encoding in 7-bit environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 8, + "alignment": 1, + "codePointCount": 19966, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "x-EUC-TW", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-ISO-2022-CN-GB", + "comment": "A variant of ISO-2022-CN supporting GB2312. It enables Simplified Chinese encoding in 7-bit environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 7, + "alignment": 1, + "codePointCount": 7572, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "GB2312", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-2022-JP", + "comment": "A Japanese encoding that switches between ASCII, JIS X 0201, and JIS X 0208 using escape sequences. It was widely used for email and early web content in Japan.", + "minBytesPerChar": 1, + "maxBytesPerChar": 8, + "alignment": 1, + "codePointCount": 7071, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "x-windows-iso2022jp", + "x-windows-50220", + "x-windows-50221", + "x-JIS0208", + "ISO-2022-JP-2", + "JIS_X0201", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-2022-JP-2", + "comment": "An extension of ISO-2022-JP that adds additional character sets including Chinese and Korean. It was intended for multilingual communication but saw limited adoption.", + "minBytesPerChar": 1, + "maxBytesPerChar": 9, + "alignment": 1, + "codePointCount": 13138, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "x-windows-iso2022jp", + "x-windows-50220", + "x-windows-50221", + "x-JIS0208", + "ISO-2022-JP", + "JIS_X0201", + "JIS_X0212-1990", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-2022-KR", + "comment": "A Korean encoding that switches between ASCII and KS X 1001 using escape sequences. It was used in email systems that required 7-bit compatibility.", + "minBytesPerChar": 1, + "maxBytesPerChar": 7, + "alignment": 1, + "codePointCount": 8354, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "EUC-KR", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-8859-1", + "comment": "A widely used Western European encoding also known as Latin-1. It supports major Western European languages but lacks the Euro symbol.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "x-iso-8859-11", + "comment": "A Thai encoding similar to TIS-620 but with additional control characters. It supports Thai script without shaping.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 247, + "scripts": [ + "COMMON", + "LATIN", + "THAI" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-8859-13", + "comment": "A Baltic language encoding supporting Latvian, Lithuanian, and Estonian. It includes diacritics needed for regional languages.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "ISO-8859-15", + "comment": "A revision of Latin-1 that adds the Euro symbol and replaces some rarely used characters. It was intended as a modernized Western European encoding.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "ISO-8859-16", + "comment": "A South-Eastern European encoding supporting Romanian, Albanian, and related languages. It includes characters missing from earlier ISO-8859 sets.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "ISO-8859-2", + "comment": "A Central European encoding supporting Polish, Czech, Slovak, and Hungarian. It includes diacritics used in Slavic and Finno-Ugric languages.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "ISO-8859-3", + "comment": "A South European encoding supporting Maltese, Esperanto, and Turkish. It was less widely adopted than other ISO-8859 variants.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 248, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-8859-4", + "comment": "A Baltic encoding supporting Estonian, Latvian, and Lithuanian. It was later superseded by ISO-8859-13.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "ISO-8859-5", + "comment": "A Cyrillic encoding supporting Russian, Bulgarian, and Serbian. It was used before Unicode and Windows-1251 became dominant.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "ISO-8859-6", + "comment": "An Arabic encoding supporting basic Arabic letters. It does not include contextual shaping, making it unsuitable for display without additional processing.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 210, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC", + "INHERITED" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-8859-7", + "comment": "A Greek encoding supporting modern Greek characters. It was widely used before Unicode adoption.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 252, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-8859-8", + "comment": "A Hebrew encoding supporting Hebrew letters. It does not include vowel points or right-to-left control characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 219, + "scripts": [ + "COMMON", + "LATIN", + "HEBREW" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "ISO-8859-9", + "comment": "A Turkish encoding based on Latin-1 but replacing Icelandic letters with Turkish ones. It supports modern Turkish orthography.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "x-JIS0208", + "comment": "A Japanese character set containing thousands of Kanji and kana. It forms the basis of many Japanese encodings including Shift_JIS and EUC-JP.", + "minBytesPerChar": 2, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 6879, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "JIS_X0201", + "comment": "A Japanese encoding containing ASCII and half-width Katakana. It is often used as part of larger Japanese encoding schemes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 192, + "scripts": [ + "COMMON", + "LATIN", + "KATAKANA" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "JIS_X0212-1990", + "comment": "A supplementary Japanese character set containing additional Kanji. It is used in extended Japanese encodings such as ISO-2022-JP-2.", + "minBytesPerChar": 2, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 6067, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HAN" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-Johab", + "comment": "A Korean encoding supporting full Hangul composition. It provides complete coverage of modern Hangul syllables.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 17175, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "KOI8-R", + "comment": "A Cyrillic encoding widely used in Russia and the former Soviet Union. It supports Russian, Bulgarian, and other Cyrillic languages.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "KOI8-U", + "comment": "A Cyrillic encoding designed for Ukrainian. It extends KOI8-R with additional Ukrainian letters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "x-MacArabic", + "comment": "A legacy Mac OS encoding for Arabic. It includes contextual forms for proper Arabic rendering.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 229, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC", + "INHERITED" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacCentralEurope", + "comment": "A Mac OS encoding for Central European languages. It supports diacritics used in Polish, Czech, Slovak, and Hungarian.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-MacCroatian", + "comment": "A Mac OS encoding for Croatian. It includes accented characters used in Croatian orthography.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacCyrillic", + "comment": "A Mac OS encoding for Cyrillic languages. It supports Russian, Bulgarian, and Serbian Cyrillic.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-MacDingbat", + "comment": "A Mac OS symbol encoding containing decorative and typographic symbols. It includes ornaments, arrows, and shapes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 220, + "scripts": [ + "COMMON" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacGreek", + "comment": "A Mac OS encoding for Greek. It includes Greek letters and diacritics used in modern Greek.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacHebrew", + "comment": "A Mac OS encoding for Hebrew. It supports Hebrew letters and right-to-left text.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 213, + "scripts": [ + "COMMON", + "LATIN", + "HEBREW" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacIceland", + "comment": "A Mac OS encoding for Icelandic. It includes Icelandic letters and diacritics.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacRoman", + "comment": "The primary Mac OS Roman encoding for Western European languages. It includes accented letters and typographic symbols.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacRomania", + "comment": "A Mac OS encoding for Romanian. It includes Romanian diacritics and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacSymbol", + "comment": "A Mac OS symbol encoding containing mathematical and typographic symbols. It includes arrows, operators, and Greek letters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 195, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacThai", + "comment": "A Mac OS encoding for Thai. It supports Thai script without contextual shaping.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 227, + "scripts": [ + "COMMON", + "LATIN", + "THAI" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacTurkish", + "comment": "A Mac OS encoding for Turkish. It includes Turkish-specific letters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 253, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [], + "canProduceError": true + }, + { + "name": "x-MacUkraine", + "comment": "A Mac OS encoding for Ukrainian. It includes Ukrainian Cyrillic letters and diacritics.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [], + "canProduceError": false + }, + { + "name": "x-MS932_0213", + "comment": "A Microsoft variant of Shift_JIS that includes JIS X 0213 characters. It expands the available Kanji set beyond standard Shift_JIS.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 11491, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN", + "INHERITED" + ], + "contains": [ + "windows-31j", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-MS950-HKSCS", + "comment": "A Microsoft Big5 variant including Hong Kong Supplementary Character Set mappings. It supports Cantonese-specific and government-required characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 18585, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "x-windows-950", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-MS950-HKSCS-XP", + "comment": "A Windows XP version of the HKSCS Big5 extension. It includes older mappings that differ from later HKSCS releases.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 16633, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "x-windows-950", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-mswin-936", + "comment": "A Microsoft implementation of GBK for Simplified Chinese. It supports a wide range of Chinese characters used on Windows systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 21919, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-PCK", + "comment": "A variant of Shift_JIS used on Solaris systems. It includes vendor-specific extensions for Japanese text.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7518, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "Shift_JIS", + "comment": "A major Japanese encoding combining single-byte and double-byte sequences. It supports Kanji, Hiragana, Katakana, and ASCII.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7071, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-SJIS_0213", + "comment": "A Shift_JIS variant supporting JIS X 0213 characters. It expands the Kanji repertoire for modern Japanese usage.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 11398, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN", + "INHERITED" + ], + "contains": [ + "Shift_JIS", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "TIS-620", + "comment": "The standard Thai character encoding used in Thailand. It supports Thai script without contextual shaping.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 215, + "scripts": [ + "COMMON", + "LATIN", + "THAI" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1250", + "comment": "A Central European Windows encoding supporting Polish, Czech, Slovak, and Hungarian. It includes diacritics used in Slavic languages.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 250, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1251", + "comment": "A Cyrillic Windows encoding supporting Russian, Bulgarian, and Serbian. It became the de facto standard for Cyrillic text on Windows.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 254, + "scripts": [ + "COMMON", + "LATIN", + "CYRILLIC" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1252", + "comment": "A Western European Windows encoding often mistaken for ISO-8859-1. It includes additional printable characters such as smart quotes.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 250, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1253", + "comment": "A Greek Windows encoding supporting modern Greek. It includes accented Greek letters and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 238, + "scripts": [ + "COMMON", + "LATIN", + "GREEK" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1254", + "comment": "A Turkish Windows encoding similar to ISO-8859-9. It supports Turkish-specific letters and symbols.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 248, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1255", + "comment": "A Hebrew Windows encoding supporting Hebrew letters and vowel points. It includes right-to-left control characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 232, + "scripts": [ + "COMMON", + "LATIN", + "HEBREW" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1256", + "comment": "An Arabic Windows encoding supporting Arabic letters and contextual forms. It includes characters for Persian and Urdu as well.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 255, + "scripts": [ + "COMMON", + "LATIN", + "ARABIC", + "INHERITED" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": false + }, + { + "name": "windows-1257", + "comment": "A Baltic Windows encoding supporting Latvian, Lithuanian, and Estonian. It includes regional diacritics and punctuation.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 243, + "scripts": [ + "COMMON", + "LATIN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-1258", + "comment": "A Vietnamese Windows encoding supporting tone marks and diacritics. It uses combining characters to represent Vietnamese vowels.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 246, + "scripts": [ + "COMMON", + "LATIN", + "INHERITED" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "windows-31j", + "comment": "A Microsoft extension of Shift_JIS adding vendor-specific characters. It is widely used in Japanese Windows environments.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 7528, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "JIS_X0201", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-windows-50220", + "comment": "A Windows variant of ISO-2022-JP using escape sequences for Japanese text. It supports 7-bit transport environments such as email.", + "minBytesPerChar": 1, + "maxBytesPerChar": 9, + "alignment": 1, + "codePointCount": 13308, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "x-windows-iso2022jp", + "x-windows-50221", + "x-JIS0208", + "ISO-2022-JP", + "ISO-2022-JP-2", + "JIS_X0201", + "JIS_X0212-1990", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-windows-50221", + "comment": "A Windows version of ISO-2022-JP that allows 8-bit bytes. It improves compatibility with applications that do not enforce strict 7-bit paths.", + "minBytesPerChar": 1, + "maxBytesPerChar": 9, + "alignment": 1, + "codePointCount": 13308, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "x-windows-iso2022jp", + "x-windows-50220", + "x-JIS0208", + "ISO-2022-JP", + "ISO-2022-JP-2", + "JIS_X0201", + "JIS_X0212-1990", + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-windows-874", + "comment": "A Windows encoding for Thai similar to TIS-620. It includes additional symbols used in Windows applications.", + "minBytesPerChar": 1, + "maxBytesPerChar": 1, + "alignment": 1, + "codePointCount": 224, + "scripts": [ + "COMMON", + "LATIN", + "THAI" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-windows-949", + "comment": "A Windows encoding for Korean based on EUC-KR. It includes extended Hangul syllables and vendor-specific characters.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 17175, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HANGUL", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-windows-950", + "comment": "A Windows encoding for Traditional Chinese based on Big5. It includes additional characters used in Taiwanese and Hong Kong systems.", + "minBytesPerChar": 1, + "maxBytesPerChar": 2, + "alignment": 1, + "codePointCount": 13620, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "BOPOMOFO", + "HAN" + ], + "contains": [ + "US-ASCII" + ], + "canProduceError": true + }, + { + "name": "x-windows-iso2022jp", + "comment": "A Windows variant of ISO-2022-JP with modified escape sequences. It improves compatibility with Japanese email and legacy applications.", + "minBytesPerChar": 1, + "maxBytesPerChar": 8, + "alignment": 1, + "codePointCount": 7525, + "scripts": [ + "COMMON", + "LATIN", + "GREEK", + "CYRILLIC", + "HIRAGANA", + "KATAKANA", + "HAN" + ], + "contains": [ + "x-windows-50220", + "x-windows-50221", + "x-JIS0208", + "ISO-2022-JP", + "ISO-2022-JP-2", + "JIS_X0201", + "US-ASCII" + ], + "canProduceError": true + } + ] +} \ No newline at end of file diff --git a/Ghidra/Framework/SoftwareModeling/data/charset_info.xml b/Ghidra/Framework/SoftwareModeling/data/charset_info.xml deleted file mode 100644 index dbf1701f79..0000000000 --- a/Ghidra/Framework/SoftwareModeling/data/charset_info.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/SoftwareModelingInitializer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/SoftwareModelingInitializer.java index d0d515fb08..207ddf1b12 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/SoftwareModelingInitializer.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/SoftwareModelingInitializer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,13 +16,13 @@ package ghidra; import ghidra.framework.ModuleInitializer; -import ghidra.program.model.data.CharsetInfo; +import ghidra.util.charset.CharsetInfoManager; public class SoftwareModelingInitializer implements ModuleInitializer { @Override public void run() { // Register user defined Charsets - CharsetInfo.reinitializeWithUserDefinedCharsets(); + CharsetInfoManager.reinitializeWithUserDefinedCharsets(); } @Override diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharDataType.java index 18be5b3488..80b0a16a9e 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharDataType.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,6 +19,7 @@ import ghidra.docking.settings.*; import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.mem.MemoryAccessException; import ghidra.util.StringFormat; +import ghidra.util.charset.CharsetInfoManager; import ghidra.util.classfinder.ClassTranslator; /** @@ -200,9 +201,9 @@ public class CharDataType extends AbstractIntegerDataType implements DataTypeWit return CharsetSettingsDefinition.CHARSET.getCharset(settings, StringDataInstance.DEFAULT_CHARSET_NAME); case 2: - return CharsetInfo.UTF16; + return CharsetInfoManager.UTF16; case 4: - return CharsetInfo.UTF32; + return CharsetInfoManager.UTF32; default: return StringDataInstance.DEFAULT_CHARSET_NAME; } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetInfo.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetInfo.java deleted file mode 100644 index 4ba8d79686..0000000000 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetInfo.java +++ /dev/null @@ -1,210 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.program.model.data; - -import java.util.*; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -import org.jdom.*; -import org.jdom.input.SAXBuilder; - -import generic.jar.ResourceFile; -import ghidra.framework.Application; -import ghidra.util.Msg; -import ghidra.util.xml.XmlUtilities; - -/** - * Additional information about {@link Charset java.nio.charset.Charset's} that - * Ghidra needs to be able to create Ghidra string datatype instances. - *

    - * See charset_info.xml to specify a custom charset. - */ -public class CharsetInfo { - - private static final class Singleton { - private static final CharsetInfo INSTANCE = new CharsetInfo(); - } - - /** - * Get the global singleton instance of this {@link CharsetInfo}. - * - * @return global singleton instance - */ - public static CharsetInfo getInstance() { - return Singleton.INSTANCE; - } - - //----------------------------------------------------------------------------- - - public static final String UTF8 = "UTF-8"; - public static final String UTF16 = "UTF-16"; - public static final String UTF32 = "UTF-32"; - public static final String USASCII = "US-ASCII"; - - /** - * @param charsetName name of charset - * @return true if the supported multi-byte charset does not specify LE or - * BE - */ - public static boolean isBOMCharset(String charsetName) { - return UTF32.equals(charsetName) || UTF16.equals(charsetName); - } - - //----------------------------------------------------------------------------- - - private Map charsetInfoRecsByName = new HashMap<>(); - private List charsetNamesList = new ArrayList<>(); - private String[] charsetNames; - - private CharsetInfo() { - initialize(false); - } - - private void initialize(boolean includeConfigFile) { - registerStandardCharsets(); - if (includeConfigFile) { - readConfigFile(); - } - addJVMAvailableCharsets(); - this.charsetNames = charsetNamesList.toArray(new String[charsetNamesList.size()]); - } - - /** - * Reinitialize registered Charsets and include user defined Charsets - * specified in charset_info.xml. - */ - public static void reinitializeWithUserDefinedCharsets() { - getInstance().initialize(true); - } - - /** - * Register minimal set of Java Charsets to facilitate most test without - * Application initialization overhead. - */ - private void registerStandardCharsets() { - if (charsetInfoRecsByName.isEmpty()) { - addCharset(USASCII, 1); - addCharset(UTF8, 1); - addCharset("ISO-8859-1", 1); - addCharset(UTF16, 2); - addCharset("UTF-16BE", 2); - addCharset("UTF-16LE", 2); - addCharset(UTF32, 4); - addCharset("UTF-32BE", 4); - addCharset("UTF-32LE", 4); - } - } - - private void addCharset(String name, int charSize) { - CharsetInfoRec rec = new CharsetInfoRec(name, charSize); - charsetInfoRecsByName.put(name, rec); - charsetNamesList.add(name); - } - - /** - * Returns an array list of the currently configured charsets. - * - * @return String[] of current configured charsets. - */ - public String[] getCharsetNames() { - return charsetNames; - } - - /** - * Returns the number of bytes that the specified charset needs to specify a - * character. - * - * @param charsetName charset name - * @return number of bytes in a character, ie. 1, 2, 4, etc, defaults to 1 - * if charset is unknown or not specified in config file. - */ - public int getCharsetCharSize(String charsetName) { - CharsetInfoRec rec = charsetInfoRecsByName.get(charsetName); - return (rec != null) ? rec.charSize : 1; - } - - /** - * Returns list of {@link Charset}s that encode with the number of bytes specified. - * @param size the number of bytes for the {@link Charset} encoding. - * @return Charsets that encode one byte characters. - */ - public List getCharsetNamesWithCharSize(int size) { - List names = new ArrayList<>(); - for (String name : charsetNames) { - if (getCharsetCharSize(name) == size) { - names.add(name); - } - } - return names; - } - - private void addJVMAvailableCharsets() { - // Add charsets that can be discovered in the current JVM. - for (String csName : Charset.availableCharsets().keySet()) { - if (charsetInfoRecsByName.containsKey(csName)) { - continue; - } - addCharset(csName, 1); - } - } - - @SuppressWarnings("unchecked") - private void readConfigFile() { - ResourceFile xmlFile = Application.findDataFileInAnyModule("charset_info.xml"); - try (InputStream xmlInputStream = xmlFile.getInputStream()) { - SAXBuilder sax = XmlUtilities.createSecureSAXBuilder(false, false); - Document doc = sax.build(xmlInputStream); - Element root = doc.getRootElement(); - for (Element child : (List) root.getChildren("charset")) { - try { - String name = child.getAttributeValue("name"); - if (name == null || name.trim().isEmpty()) { - throw new IOException("Bad charset definition in " + xmlFile); - } - if (!Charset.isSupported(name)) { - Msg.warn(this, - "Unsupported charset defined in " + xmlFile.getName() + ": " + name); - } - - int charSize = XmlUtilities.parseBoundedIntAttr(child, "charSize", 1, 8); - - addCharset(name, charSize); - } - catch (NumberFormatException nfe) { - throw new IOException("Invalid charset definition in " + xmlFile); - } - } - } - catch (JDOMException | IOException e) { - Msg.showError(this, null, "Error reading charset data", e.getMessage(), e); - } - - } - - private static class CharsetInfoRec { - final String name; - final int charSize; - - CharsetInfoRec(String name, int charSize) { - this.name = name; - this.charSize = charSize; - } - } - -} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetSettingsDefinition.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetSettingsDefinition.java index c804adba0c..0b7184f516 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetSettingsDefinition.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/CharsetSettingsDefinition.java @@ -18,6 +18,8 @@ package ghidra.program.model.data; import java.util.*; import ghidra.docking.settings.*; +import ghidra.util.charset.CharsetInfo; +import ghidra.util.charset.CharsetInfoManager; /** * {@link SettingsDefinition} for setting the charset of a string instance. @@ -56,7 +58,7 @@ public class CharsetSettingsDefinition implements EnumSettingsDefinition { private final Map stringToOrdinal = new HashMap<>(); private CharsetSettingsDefinition() { - ordinalToString = CharsetInfo.getInstance().getCharsetNames(); + ordinalToString = CharsetInfoManager.getInstance().getCharsetNames().toArray(String[]::new); for (int i = 0; i < ordinalToString.length; i++) { stringToOrdinal.put(ordinalToString[i], i); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/PascalUnicodeDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/PascalUnicodeDataType.java index a51d32b914..75fc11185c 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/PascalUnicodeDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/PascalUnicodeDataType.java @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import ghidra.util.charset.CharsetInfoManager; + /** * A length-prefixed string {@link DataType} (max 64k bytes) with char size of 2 bytes, * {@link CharsetSettingsDefinition UTF-16} charset, unbounded @@ -35,7 +37,7 @@ public class PascalUnicodeDataType extends AbstractStringDataType { "P_UNI", // default label prefix "pu", // default abbrev label prefix "String (Pascal UTF-16 64k)", // description - CharsetInfo.UTF16, // charset + CharsetInfoManager.UTF16, // charset ByteDataType.dataType, // replacement data type StringLayoutEnum.PASCAL_64k, // StringLayoutEnum dtm// data type manager diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java index 0faa9d09df..e2cfc5dd70 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringDataInstance.java @@ -34,6 +34,7 @@ import ghidra.program.model.lang.Endian; import ghidra.program.model.listing.Data; import ghidra.program.model.mem.*; import ghidra.util.*; +import ghidra.util.charset.CharsetInfoManager; /** * Represents an instance of a string in a {@link MemBuffer}. @@ -251,7 +252,7 @@ public class StringDataInstance { public static final int MAX_STRING_LENGTH = 16 * 1024; - public static final String DEFAULT_CHARSET_NAME = CharsetInfo.USASCII; + public static final String DEFAULT_CHARSET_NAME = CharsetInfoManager.USASCII; public static final String UNKNOWN = "??"; public static final String UNKNOWN_DOT_DOT_DOT = "??..."; @@ -322,7 +323,7 @@ public class StringDataInstance { settings = (settings == null) ? SettingsImpl.NO_SETTINGS : settings; this.buf = buf; this.charsetName = getCharsetNameFromDataTypeOrSettings(dataType, settings); - this.charSize = CharsetInfo.getInstance().getCharsetCharSize(charsetName); + this.charSize = CharsetInfoManager.getInstance().getCharsetCharSize(charsetName); // NOTE: for now only handle padding for charSize == 1 and the data type is an array of elements, not a "string" this.paddedCharSize = (dataType instanceof ArrayStringable) && (charSize == 1) // ? getDataOrganization(dataType).getCharSize() @@ -729,7 +730,7 @@ public class StringDataInstance { private AdjustedCharsetInfo getAdjustedCharsetInfo(byte[] bytes) { AdjustedCharsetInfo result = new AdjustedCharsetInfo(charsetName); - if (CharsetInfo.isBOMCharset(charsetName)) { + if (CharsetInfoManager.isBOMCharset(charsetName)) { result.endian = getEndiannessFromBOM(bytes, charSize); if (result.endian != null) { // skip the BOM char when creating the string @@ -753,7 +754,7 @@ public class StringDataInstance { private String getAdjustedCharsetInfo(ByteBuffer bb) { String result = charsetName; - if (CharsetInfo.isBOMCharset(charsetName)) { + if (CharsetInfoManager.isBOMCharset(charsetName)) { Endian endian = getEndiannessFromBOM(bb, charSize); if (endian == null) { endian = endianSetting; @@ -1030,15 +1031,18 @@ public class StringDataInstance { dataTypeMap.put(new Pair<>(NULL_TERMINATED_UNBOUNDED, null), TerminatedStringDataType.dataType); - dataTypeMap.put(new Pair<>(PASCAL_64k, CharsetInfo.UTF16), PascalUnicodeDataType.dataType); + dataTypeMap.put(new Pair<>(PASCAL_64k, CharsetInfoManager.UTF16), + PascalUnicodeDataType.dataType); - dataTypeMap.put(new Pair<>(FIXED_LEN, CharsetInfo.UTF8), StringUTF8DataType.dataType); - dataTypeMap.put(new Pair<>(FIXED_LEN, CharsetInfo.UTF16), UnicodeDataType.dataType); - dataTypeMap.put(new Pair<>(FIXED_LEN, CharsetInfo.UTF32), Unicode32DataType.dataType); + dataTypeMap.put(new Pair<>(FIXED_LEN, CharsetInfoManager.UTF8), + StringUTF8DataType.dataType); + dataTypeMap.put(new Pair<>(FIXED_LEN, CharsetInfoManager.UTF16), UnicodeDataType.dataType); + dataTypeMap.put(new Pair<>(FIXED_LEN, CharsetInfoManager.UTF32), + Unicode32DataType.dataType); - dataTypeMap.put(new Pair<>(NULL_TERMINATED_UNBOUNDED, CharsetInfo.UTF16), + dataTypeMap.put(new Pair<>(NULL_TERMINATED_UNBOUNDED, CharsetInfoManager.UTF16), TerminatedUnicodeDataType.dataType); - dataTypeMap.put(new Pair<>(NULL_TERMINATED_UNBOUNDED, CharsetInfo.UTF32), + dataTypeMap.put(new Pair<>(NULL_TERMINATED_UNBOUNDED, CharsetInfoManager.UTF32), TerminatedUnicode32DataType.dataType); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringRenderParser.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringRenderParser.java index 4672f88911..315571ea30 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringRenderParser.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringRenderParser.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import ghidra.program.model.lang.Endian; import ghidra.util.StringUtilities; +import ghidra.util.charset.CharsetInfoManager; import ghidra.util.exception.UsrException; /** @@ -60,18 +61,18 @@ public class StringRenderParser { private final boolean isFinal; private final Set accepts; - private State(boolean isFinal) { // Implies this accepts any + State(boolean isFinal) { // Implies this accepts any this.isFinal = isFinal; this.accepts = null; } - private State(boolean isFinal, String accepts) { + State(boolean isFinal, String accepts) { this(isFinal, accepts.chars() .mapToObj(i -> (char) i) .collect(Collectors.toSet())); } - private State(boolean isFinal, Set accepts) { + State(boolean isFinal, Set accepts) { this.isFinal = isFinal; this.accepts = Collections.unmodifiableSet(accepts); } @@ -140,8 +141,8 @@ public class StringRenderParser { protected void initCharset(ByteBuffer out, String reprCharsetName) { String charsetName = this.charsetName != null ? this.charsetName : reprCharsetName; - int charSize = CharsetInfo.getInstance().getCharsetCharSize(charsetName); - if (CharsetInfo.isBOMCharset(charsetName)) { + int charSize = CharsetInfoManager.getInstance().getCharsetCharSize(charsetName); + if (CharsetInfoManager.isBOMCharset(charsetName)) { // Take care of the BOM ourselves, because it must be first, before any initial bytes charsetName += endian.isBigEndian() ? "BE" : "LE"; } @@ -175,19 +176,19 @@ public class StringRenderParser { return State.PREFIX; } if (c == 'U') { - initCharset(out, CharsetInfo.UTF32); + initCharset(out, CharsetInfoManager.UTF32); return State.UNIT; } - initCharset(out, CharsetInfo.USASCII); + initCharset(out, CharsetInfoManager.USASCII); return parseCharUnit(out, c); } protected State parseCharPrefix(ByteBuffer out, char c) { if (c == '8') { - initCharset(out, CharsetInfo.UTF8); + initCharset(out, CharsetInfoManager.UTF8); return State.UNIT; } - initCharset(out, CharsetInfo.UTF16); + initCharset(out, CharsetInfoManager.UTF16); return parseCharUnit(out, c); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringUTF8DataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringUTF8DataType.java index 8b540b2a8f..9fc13f41f2 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringUTF8DataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/StringUTF8DataType.java @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import ghidra.util.charset.CharsetInfoManager; + /** * A fixed-length UTF-8 string {@link DataType}. */ @@ -32,7 +34,7 @@ public class StringUTF8DataType extends AbstractStringDataType { "STR", // default label prefix "s", // default abbrev label prefix "String (Fixed Length UTF-8 Unicode)", // description - CharsetInfo.UTF8, // charset + CharsetInfoManager.UTF8, // charset CharDataType.dataType, // replacement data type StringLayoutEnum.FIXED_LEN, // StringLayoutEnum dtm// data type manager diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicode32DataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicode32DataType.java index d8a4da322c..31532e49f3 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicode32DataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicode32DataType.java @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import ghidra.util.charset.CharsetInfoManager; + /** * A null-terminated UTF-32 string {@link DataType}. */ @@ -33,7 +35,7 @@ public class TerminatedUnicode32DataType extends AbstractStringDataType { "UNI", // default label prefix "u", // default abbrev label prefix "String (Null Terminated UTF-32 Unicode)", // description - CharsetInfo.UTF32, // charset + CharsetInfoManager.UTF32, // charset WideChar32DataType.dataType, // replacement data type StringLayoutEnum.NULL_TERMINATED_UNBOUNDED, // StringLayoutEnum dtm// data type manager diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicodeDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicodeDataType.java index 74407f9534..0730c582f9 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicodeDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/TerminatedUnicodeDataType.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import ghidra.util.charset.CharsetInfoManager; + /** * A null-terminated string {@link DataType} with a UTF-16 {@link CharsetSettingsDefinition charset}. *

    @@ -38,7 +40,7 @@ public class TerminatedUnicodeDataType extends AbstractStringDataType { DEFAULT_UNICODE_LABEL_PREFIX, // default label prefix DEFAULT_UNICODE_ABBREV_PREFIX, // default abbrev label prefix "String (Null Terminated UTF-16 Unicode)", // description - CharsetInfo.UTF16, // charset + CharsetInfoManager.UTF16, // charset WideChar16DataType.dataType, // replacement data type StringLayoutEnum.NULL_TERMINATED_UNBOUNDED, // StringLayoutEnum dtm// data type manager diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Unicode32DataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Unicode32DataType.java index e16bd559da..ae01e89bfc 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Unicode32DataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Unicode32DataType.java @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import ghidra.util.charset.CharsetInfoManager; + /** * A fixed-length UTF-32 string {@link DataType}. */ @@ -36,7 +38,7 @@ public class Unicode32DataType extends AbstractStringDataType { "UNI", // default label prefix "u", // default abbrev label prefix "String (Fixed Length UTF-32 Unicode)", // description - CharsetInfo.UTF32, // charset + CharsetInfoManager.UTF32, // charset WideChar32DataType.dataType, // replacement data type StringLayoutEnum.FIXED_LEN, // StringLayoutEnum dtm// data type manager diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/UnicodeDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/UnicodeDataType.java index 6a92374ddc..da2563c21b 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/UnicodeDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/UnicodeDataType.java @@ -15,6 +15,8 @@ */ package ghidra.program.model.data; +import ghidra.util.charset.CharsetInfoManager; + /** * A fixed-length UTF-16 string {@link DataType}. */ @@ -33,7 +35,7 @@ public class UnicodeDataType extends AbstractStringDataType { "UNI", // default label prefix "u", // default abbrev label prefix "String (Fixed Length UTF-16 Unicode)", // description - CharsetInfo.UTF16, // charset + CharsetInfoManager.UTF16, // charset WideChar16DataType.dataType, // replacement data type StringLayoutEnum.FIXED_LEN, // StringLayoutEnum dtm// data type manager diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar16DataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar16DataType.java index 079c442ff2..f43ba74122 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar16DataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar16DataType.java @@ -20,6 +20,7 @@ import ghidra.docking.settings.SettingsDefinition; import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.mem.MemoryAccessException; import ghidra.util.StringUtilities; +import ghidra.util.charset.CharsetInfoManager; public class WideChar16DataType extends BuiltIn implements ArrayStringable, DataTypeWithCharset { @@ -152,6 +153,6 @@ public class WideChar16DataType extends BuiltIn implements ArrayStringable, Data @Override public String getCharsetName(Settings settings) { - return CharsetInfo.UTF16; + return CharsetInfoManager.UTF16; } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar32DataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar32DataType.java index 1ffe2eda4a..db11bf3680 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar32DataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideChar32DataType.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,6 +21,7 @@ import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.model.scalar.Scalar; import ghidra.util.StringUtilities; +import ghidra.util.charset.CharsetInfoManager; public class WideChar32DataType extends BuiltIn implements ArrayStringable, DataTypeWithCharset { @@ -154,7 +155,7 @@ public class WideChar32DataType extends BuiltIn implements ArrayStringable, Data @Override public String getCharsetName(Settings settings) { - return CharsetInfo.UTF32; + return CharsetInfoManager.UTF32; } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideCharDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideCharDataType.java index f82d8469c7..fa68b29b80 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideCharDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/WideCharDataType.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,6 +21,7 @@ import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.model.scalar.Scalar; import ghidra.util.StringUtilities; +import ghidra.util.charset.CharsetInfoManager; public class WideCharDataType extends BuiltIn implements ArrayStringable, DataTypeWithCharset { final static SettingsDefinition[] DEFAULT_WIDE_CHAR_SETTINGS = new SettingsDefinition[] { @@ -186,9 +187,9 @@ public class WideCharDataType extends BuiltIn implements ArrayStringable, DataTy public String getCharsetName(Settings settings) { switch (getLength()) { case 2: - return CharsetInfo.UTF16; + return CharsetInfoManager.UTF16; case 4: - return CharsetInfo.UTF32; + return CharsetInfoManager.UTF32; default: return StringDataInstance.DEFAULT_CHARSET_NAME; } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfo.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfo.java new file mode 100644 index 0000000000..047a11a1a3 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfo.java @@ -0,0 +1,192 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset; + +import java.lang.Character.UnicodeScript; +import java.nio.charset.Charset; +import java.util.*; + +import generic.json.Json; + +/** + * Additional information about {@link Charset java.nio.charset.Charset's} that + * Ghidra needs to be able to create Ghidra string datatype instances. + *

    + * See charset_info.json to specify info about a custom charset. + */ +public class CharsetInfo { + static final Set FIELDS_TO_EXCLUDE_FROM_JSON = Set.of("standardCharset"); + static final int UNICODESCRIPT_COUNT = UnicodeScript.UNKNOWN.ordinal() + 1; + static final EnumSet ALL_SCRIPTS = EnumSet.allOf(UnicodeScript.class); + static final EnumSet NO_SCRIPTS = EnumSet.noneOf(UnicodeScript.class); + + private final String name; + private final String comment; + private final int minBytesPerChar; + private final int maxBytesPerChar; + private final int alignment; + private final int codePointCount; + private final EnumSet scripts; + private final Set contains; + private final boolean canProduceError; + private final boolean standardCharset; // not serialized, see FIELDS_TO_EXCLUDE_FROM_JSON + + public CharsetInfo(Charset cs) { + this(cs.name(), null, 1, -1, 1, -1, false, true, NO_SCRIPTS, Set.of()); + } + + public CharsetInfo(String name, String comment, int minBytesPerChar, int maxBytesPerChar, + int alignment, int codePointCount, boolean standardCharset, boolean canProduceError, + EnumSet scripts, Set contains) { + this.name = name; + this.comment = comment; + this.minBytesPerChar = minBytesPerChar; + this.maxBytesPerChar = maxBytesPerChar; + this.alignment = alignment; + this.codePointCount = codePointCount; + this.standardCharset = standardCharset; + this.canProduceError = canProduceError; + this.scripts = scripts; + this.contains = contains; + } + + /** + * {@return a copy of this instance, with a new comment value} + * @param newComment string + */ + public CharsetInfo withComment(String newComment) { + return new CharsetInfo(name, newComment, minBytesPerChar, maxBytesPerChar, alignment, + codePointCount, standardCharset, canProduceError, scripts, contains); + } + + /** + * @return {@link Charset} + */ + public Charset getCharset() { + return Charset.forName(name, null); + } + + /** + * {@return name of the charset} + */ + public String getName() { + return name; + } + + /** + * {@return boolean flag, true if this is a standard charset that is guaranteed to be present + * in the jvm, otherwise false} + */ + public boolean isStandardCharset() { + return standardCharset; + } + + /** + * {@return true if this charset can produce Unicode REPLACEMENT codepoints for + * bad byte sequences, otherwise false if there are no byte sequences that result in REPLACEMENT + * codepoints. This is typically single-byte charsets that map all byte values to a codepoint} + */ + public boolean isCanProduceError() { + return canProduceError; + } + + /** + * {@return true if this charset can produce Unicode codepoints that are in all scripts} + */ + public boolean supportsAllScripts() { + return scripts.size() >= UNICODESCRIPT_COUNT - 1 /* ignore unknown */; + } + + /** + * {@return the UnicodeScripts that this charset can produce} + */ + public Set getScripts() { + return scripts; + } + + /** + * {@return true if this charset only consumes a fixed number of bytes per output codepoint} + */ + public boolean hasFixedLengthChars() { + return minBytesPerChar > 0 && minBytesPerChar == maxBytesPerChar; + } + + /** + * {@return the alignment value for this charset, typically 1 for most charsets, but for + * well-known fixed-width charsets, it will return those charsets fixed-width} + */ + public int getAlignment() { + return alignment; + } + + /** + * {@return the smallest number of bytes needed to produce a codepoint} + */ + public int getMinBytesPerChar() { + return minBytesPerChar; + } + + /** + * {@return the largest number of bytes needed to produce a codepoint} + */ + public int getMaxBytesPerChar() { + return maxBytesPerChar; + } + + /** + * {@return the number of codepoints that this charset can produce} + */ + public int getCodePointCount() { + return codePointCount; + } + + /** + * Returns the names of other charsets that this charset {@link Charset#contains(Charset)}. + * + * @return names of other charsets + */ + public Set getContains() { + return contains; + } + + /** + * {@return a string comment describing this charset, or null} + */ + public String getComment() { + return comment; + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CharsetInfo other)) { + return false; + } + return Objects.equals(name, other.name); + } + + @Override + public String toString() { + return Json.toString(this); + } +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfoManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfoManager.java new file mode 100644 index 0000000000..85f9ad6043 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/util/charset/CharsetInfoManager.java @@ -0,0 +1,375 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset; + +import static java.lang.Character.UnicodeScript.*; +import static java.nio.charset.StandardCharsets.*; + +import java.io.*; +import java.lang.Character.UnicodeScript; +import java.nio.charset.Charset; +import java.util.*; + +import com.google.gson.*; +import com.google.gson.stream.JsonReader; + +import generic.jar.ResourceFile; +import ghidra.framework.Application; +import ghidra.util.Msg; + +/** + * Maintains a list of charsets and info about each charset. More common charsets are ordered + * toward the beginning of the list. + *

    + * Created instances are immutable, but the "INSTANCE" singleton can be replaced by a new value + * when {@link #reinitializeWithUserDefinedCharsets()} is called. (This is done to avoid reading + * the user config file and causing slow downs during certain stages of the startup) + */ +public class CharsetInfoManager { + public static final String UTF8 = "UTF-8"; + public static final String UTF16 = "UTF-16"; + public static final String UTF32 = "UTF-32"; + public static final String USASCII = "US-ASCII"; + + /** + * Comparator that ignores charset name "x-" prefixes + */ + public static Comparator CHARSET_NAME_COMP = (s1, s2) -> { + return stripCharsetX(s1).compareToIgnoreCase(stripCharsetX(s2)); + }; + + /** + * Comparator that ignores charset name "x-" prefixes + */ + public static Comparator CHARSET_COMP = (csi1, csi2) -> { + return stripCharsetX(csi1.getName()).compareToIgnoreCase(stripCharsetX(csi2.getName())); + }; + + private static final class Singleton { + private static CharsetInfoManager INSTANCE = new CharsetInfoManager(); + } + + private static final class CharSetsSingleton { + // decouple loading these 'non-standard' (but actually standard) charsets to a different + // class so their initialization can happen before the manager singleton instance + private static final Charset UTF_32 = Charset.forName(UTF32); + private static final Charset UTF_32LE = Charset.forName("UTF-32LE"); + private static final Charset UTF_32BE = Charset.forName("UTF-32BE"); + } + + /** + * Get the global singleton instance of this {@link CharsetInfoManager}. + *

    + * This singleton will only have generic information until + * {@link CharsetInfoManager#reinitializeWithUserDefinedCharsets()} is called. + * + * @return global singleton instance + */ + public static CharsetInfoManager getInstance() { + return Singleton.INSTANCE; + } + + /** + * {@return true if the specified charset needs additional care for handling byte-order-mark + * byte values (eg. UTF-16/32). If the charset is a LE/BE variant, no extra care is needed.} + * @param charsetName name of charset + */ + public static boolean isBOMCharset(String charsetName) { + return UTF32.equals(charsetName) || UTF16.equals(charsetName); + } + + private static String stripCharsetX(String csName) { + return csName.startsWith("x-") ? csName.substring(2) : csName; + } + + private Map charsets = new LinkedHashMap<>(); // preserve addition order + + private CharsetInfoManager() { + this(List.of()); + } + + private CharsetInfoManager(List userDefinedInfo) { + // add ASCII+UTF-NN charsets first (these are the most commonly used) + getStandardCharsets().forEach(csi -> charsets.put(csi.getName(), csi)); + + // add user defined charsets (that are present in the jvm) + userDefinedInfo.forEach(csi -> { + if (Charset.isSupported(csi.getName())) { + charsets.put(csi.getName(), csi); + } + }); + + // last, add any charsets that are not covered by the standard and user-defined charsets + List availCSNames = new ArrayList<>(Charset.availableCharsets().keySet()); + availCSNames.sort(CHARSET_NAME_COMP); + for (String csName : availCSNames) { + if (!charsets.containsKey(csName)) { + charsets.put(csName, new CharsetInfo(Charset.forName(csName))); + } + } + } + + /** + * {@return List of names of current configured charsets} + */ + public List getCharsetNames() { + return List.copyOf(charsets.keySet()); + } + + /** + * {@return list of all available charsets} + */ + public List getCharsets() { + return List.copyOf(charsets.values()); + } + + /** + * Returns the number of bytes that the specified charset needs to specify a + * character. + * + * @param charsetName charset name + * @return number of bytes in a character, ie. 1, 2, 4, etc, defaults to 1 + * if charset is unknown or not specified in config file. + */ + public int getCharsetCharSize(String charsetName) { + CharsetInfo csi = charsets.get(charsetName); + return (csi != null) ? csi.getMinBytesPerChar() : 1; + } + + /** + * Returns list of {@link Charset}s that encode with the number of bytes specified. + * @param size the number of bytes for the {@link Charset} encoding. + * @return Charsets that encode one byte characters. + */ + public List getCharsetNamesWithCharSize(int size) { + return charsets.values() + .stream() + .filter(csi -> csi.getMinBytesPerChar() == size) + .map(csi -> csi.getName()) + .toList(); + } + + /** + * {@return charset info object that represents the specified charset} + * @param cs charset + */ + public CharsetInfo get(Charset cs) { + return charsets.get(cs.name()); + } + + /** + * {@return charset info object that represents the specified charset} + * @param name charset name + */ + public CharsetInfo get(String name) { + return charsets.get(name); + } + + /** + * {@return charset info object that represents the specified charset, and if not found, + * returning the defaultCS value} + * + * @param name charset name + * @param defaultCS default value to return if not found + */ + public CharsetInfo get(String name, Charset defaultCS) { + CharsetInfo result = charsets.get(name); + if (result == null && defaultCS != null) { + result = charsets.get(defaultCS.name()); + } + return result; + } + + /** + * {@return a hopefully short list of non-LATIN UnicodeScripts that are supported by a + * charset that is present in this jvm. (ignoring any charsets that support all scripts). + * This list of scripts can be useful when presenting the user with a list of scripts or + * things related to a script. Typically the list will contain: + * ARABIC, BOPOMOFO, CYRILLIC, DEVANAGARI, HANGUL, HAN, HEBREW, HIRAGANA, KATAKANA, THAI } + */ + public List getMostImplementedScripts() { + Set scriptsToIgnore = EnumSet.of(COMMON, INHERITED, UNKNOWN, LATIN, GREEK); + List scripts = charsets.values() + .stream() + .filter(csi -> !csi.supportsAllScripts()) + .flatMap(csi -> csi.getScripts().stream()) + .filter(script -> !scriptsToIgnore.contains(script)) + .sorted((o1, o2) -> o1.name().compareTo(o2.name())) + .distinct() + .toList(); + return scripts; + } + + //--------------------------------------------------------------------------------------------- + // static helper methods + //--------------------------------------------------------------------------------------------- + + public static List getStandardCharsetNames() { + return List.of(USASCII, UTF8, UTF16, UTF32); + } + + private static List getStandardCharsets() { + //@formatter:off + return List.of( + new CharsetInfo(USASCII, null, 1, 1, 1, -1, true, true, EnumSet.of(COMMON, LATIN), Set.of()), + new CharsetInfo(UTF8, null, 1, 4, 1, -1, true, true, CharsetInfo.ALL_SCRIPTS, Set.of()), + + new CharsetInfo(UTF16, null, 2, 4, 2, -1, true, true, CharsetInfo.ALL_SCRIPTS, Set.of()), + new CharsetInfo(UTF_16BE.name(), null, 2, 4, 2, -1, true, true, CharsetInfo.ALL_SCRIPTS, Set.of()), + new CharsetInfo(UTF_16LE.name(), null, 2, 4, 2, -1, true, true, CharsetInfo.ALL_SCRIPTS, Set.of()), + + new CharsetInfo(CharSetsSingleton.UTF_32.name(), null, 4, 4, 4, -1, true, true, CharsetInfo.ALL_SCRIPTS, Set.of()), + new CharsetInfo(CharSetsSingleton.UTF_32BE.name(), null, 4, 4, 4, -1, true, true, CharsetInfo.ALL_SCRIPTS, Set.of()), + new CharsetInfo(CharSetsSingleton.UTF_32LE.name(), null, 4, 4, 4, -1, true, true, CharsetInfo.ALL_SCRIPTS, Set.of()), + + new CharsetInfo(ISO_8859_1.name(), null, 1, 1, 1, -1, true, false, EnumSet.of(COMMON, LATIN), Set.of(USASCII)) + ); + //@formatter:on + } + + /** + * Replaces the current singleton with a new singleton that has been initialized with the + * optional information found in the charset_info.json file. + */ + public static void reinitializeWithUserDefinedCharsets() { + CharsetInfoConfigFile configFile = CharsetInfoConfigFile.read(getConfigFileLocation()); + if (!configFile.getCharsets().isEmpty()) { + Singleton.INSTANCE = new CharsetInfoManager(configFile.getCharsets()); + } + } + + /** + * {@return filename of the config file} + */ + public static ResourceFile getConfigFileLocation() { + return Application.findDataFileInAnyModule("charset_info.json"); + } + + /** + * Class to represent the charsetinfo json configuration file. + */ + public static class CharsetInfoConfigFile { + /** + * Read config info from the specified file + * + * @param configFile {@link ResourceFile} + * @return new {@link CharsetInfoConfigFile}, never null, but maybe empty + */ + public static CharsetInfoConfigFile read(ResourceFile configFile) { + if (configFile != null) { + try (InputStream is = configFile.getInputStream(); + JsonReader reader = new JsonReader(new InputStreamReader(is))) { + + Gson gson = new GsonBuilder().create(); + CharsetInfoConfigFile configFileData = + gson.fromJson(reader, CharsetInfoConfigFile.class); + + if (configFileData == null) { + return new CharsetInfoConfigFile(); + } + + configFileData.validateData(); + return configFileData; + } + catch (JsonParseException | IOException e) { + Msg.error(CharsetInfoManager.class, "Error reading charset_info.json", e); + // fall thru, return default empty instance + } + } + return new CharsetInfoConfigFile(); + } + + private List comments; // broken up into a list so it looks good in the json + private List charsets; + + public CharsetInfoConfigFile() { + this.comments = List.of(); + this.charsets = List.of(); + } + + public CharsetInfoConfigFile(String comment, List charsets) { + this.comments = comment.lines().toList(); + this.charsets = charsets; + } + + public String getComment() { + return String.join("\n", comments); + } + + public List getCharsets() { + return charsets; + } + + public void validateData() { + Set names = new HashSet<>(); + Set dups = new HashSet<>(); + Set unknowns = new HashSet<>(); + + for (CharsetInfo csi : charsets) { + if (!names.add(csi.getName())) { + dups.add(csi.getName()); + } + if (!Charset.isSupported(csi.getName())) { + unknowns.add(csi.getName()); + } + } + if (!dups.isEmpty()) { + Msg.warn(CharsetInfoManager.class, + "Duplicate charset names found in charset_info.json: " + dups); + } + if (!unknowns.isEmpty()) { + Msg.warn(CharsetInfoManager.class, + "Unknown/unsupported charset names found in charset_info.json: " + unknowns); + } + } + + /** + * Writes this instance to a json file. + * + * @param configFilename where to write to + * @throws IOException if error writing + */ + public void write(File configFilename) throws IOException { + File configDir = configFilename.getParentFile(); + File tmpConfigFile = new File(configDir, configFilename.getName() + ".tmp"); + File prevConfigFile = new File(configDir, configFilename.getName() + ".prev"); + + try (Writer fw = new FileWriter(tmpConfigFile)) { + Gson gson = new GsonBuilder().setPrettyPrinting() + .addSerializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes f) { + return f.getDeclaringClass().equals(CharsetInfo.class) && + CharsetInfo.FIELDS_TO_EXCLUDE_FROM_JSON.contains(f.getName()); + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } + }) + .create(); + gson.toJson(this, fw); + } + prevConfigFile.delete(); + configFilename.renameTo(prevConfigFile); + if (tmpConfigFile.renameTo(configFilename)) { + prevConfigFile.delete(); + } + } + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/util/charset/CharsetInfoManagerTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/util/charset/CharsetInfoManagerTest.java new file mode 100644 index 0000000000..749806525f --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/util/charset/CharsetInfoManagerTest.java @@ -0,0 +1,170 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.charset; + +import static java.lang.Character.UnicodeScript.*; +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.lang.Character.UnicodeScript; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.*; +import java.util.*; + +import org.junit.Ignore; +import org.junit.Test; + +import generic.jar.ResourceFile; +import generic.test.AbstractGenericTest; +import ghidra.util.Msg; +import ghidra.util.StringUtilities; +import ghidra.util.charset.CharsetInfoManager.CharsetInfoConfigFile; + +public class CharsetInfoManagerTest extends AbstractGenericTest { + + @Test + public void testCharsetsArePresent() { + int jvmCharsetCount = Charset.availableCharsets().size(); + int csimCount = CharsetInfoManager.getInstance().getCharsetNames().size(); + assertEquals(jvmCharsetCount, csimCount); + } + + /** + * Generates information about the charsets that are available in the current JVM. This can + * take a couple of minutes, and only needs to be done when there are new charsets of interest. + *

    + * Attempts to preserve user-defined comments present in the previous .json file. + *

    + * To execute, comment out the Ignore annotation and run as a junit. Do not leave enabled. + * + * @throws IOException if error + */ + @Ignore("temporarily enable and run when new charsets need to be categorized") + @Test + public void generateCharsetInfoFile() throws IOException { + File configFilename = CharsetInfoManager.getConfigFileLocation().getFile(false); + + CharsetInfoConfigFile configFile = + CharsetInfoManager.CharsetInfoConfigFile.read(new ResourceFile(configFilename)); + List existingInfo = new ArrayList<>(configFile.getCharsets()); + + Msg.info(this, "Read " + existingInfo.size() + " previous records"); + + List newInfo = new ArrayList<>(); + Set standardCharsetNames = Set.copyOf(CharsetInfoManager.getStandardCharsetNames()); + for (String csName : Charset.availableCharsets().keySet()) { + if (standardCharsetNames.contains(csName) || csName.toLowerCase().contains("utf-")) { + continue; + } + Charset cs = Charset.forName(csName); + CharsetInfo csi = getCharsetInfoViaEncoder(cs); + if (csi != null) { + updateOrAppend(existingInfo, newInfo, csi); + } + Msg.info(this, "%s: %s".formatted(csName, csi != null ? "SUCCESS" : "NO INFO")); + } + + newInfo.sort(CharsetInfoManager.CHARSET_COMP); + existingInfo.addAll(newInfo); + + String comment = """ + Information about character encodings used by Ghidra. + Generated on %s by CharsetInfoManagerTest.generateCharsetInfoFile + """.formatted(new Date()); + + CharsetInfoConfigFile newConfigFile = new CharsetInfoConfigFile(comment, existingInfo); + newConfigFile.write(configFilename); + Msg.info(this, "Done"); + } + + private void updateOrAppend(List existingList, List newList, + CharsetInfo newInfo) { + for (int i = 0; i < existingList.size(); i++) { + CharsetInfo existing = existingList.get(i); + if (existing.getName().equals(newInfo.getName())) { + if (newInfo.getComment() == null && existing.getComment() != null) { + newInfo = newInfo.withComment(existing.getComment()); + } + existingList.set(i, newInfo); + return; + } + } + newList.add(newInfo); + } + + private CharsetInfo getCharsetInfoViaEncoder(Charset cs) { + // Creates a CharsetInfo by using the charset's encoder to test what happens for each + // of the 1.1M unicode codepoints. + // NOTE: trying to use a charset's decoder to decode every byte sequence into codepoints is + // not computationally feasible for byte sequences longer than 3. + EnumSet scripts = EnumSet.noneOf(UnicodeScript.class); + Set byteLens = new HashSet<>(); + int goodCPCount = 0; + CharsetEncoder encoder = null; + try { + encoder = cs.newEncoder(); + } + catch (UnsupportedOperationException e) { + return null; + } + + for (int cp = 1; cp <= Character.MAX_CODE_POINT; cp++) { + if (cp == StringUtilities.UNICODE_REPLACEMENT || UnicodeScript.of(cp) == UNKNOWN) { + continue; + } + String s = Character.toString(cp); + if (!encoder.canEncode(s)) { + continue; + } + try { + CharBuffer cb = CharBuffer.wrap(s); + ByteBuffer bb = encoder.encode(cb); + + goodCPCount++; + scripts.add(UnicodeScript.of(cp)); + byte[] bytes = new byte[bb.limit()]; + bb.get(bytes); + byteLens.add(bytes.length); + } + catch (CharacterCodingException e) { + // skip + } + } + IntSummaryStatistics stats = + byteLens.stream().mapToInt(Integer::intValue).summaryStatistics(); + + // all 255 byte values produce a valid unicode codepoint, with no error mappings possible + boolean singleByteFullyMappedCS = + stats.getMin() == 1 && stats.getMax() == 1 && goodCPCount == 255; + + CharsetInfo csi = new CharsetInfo(cs.name(), null, stats.getMin(), stats.getMax(), 1, + goodCPCount, false, !singleByteFullyMappedCS, scripts, getCSContains(cs)); + return csi; + } + + private Set getCSContains(Charset cs) { + Set result = new HashSet<>(); + for (String csName : Charset.availableCharsets().keySet()) { + if (!csName.equals(cs.name()) && cs.contains(Charset.forName(csName))) { + result.add(csName); + } + } + return result; + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ByteViewerPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ByteViewerPluginScreenShots.java index 2c9015984d..03761ab0e3 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ByteViewerPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ByteViewerPluginScreenShots.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,9 +18,10 @@ package help.screenshot; import org.junit.Test; import docking.ComponentProvider; -import docking.DialogComponentProvider; +import ghidra.app.plugin.core.byteviewer.ByteViewerComponentProvider; import ghidra.app.plugin.core.clear.ClearCmd; -import ghidra.app.util.AddressInput; +import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; +import ghidra.app.plugin.core.datamgr.DataTypesProvider; import ghidra.program.model.address.AddressSet; public class ByteViewerPluginScreenShots extends GhidraScreenShotGenerator { @@ -29,55 +30,62 @@ public class ByteViewerPluginScreenShots extends GhidraScreenShotGenerator { super(); } -@Test - public void testByteViewer() { + @Test + public void testByteViewer() { + closeProvider(DataTypesProvider.class); + closeProvider(CodeViewerProvider.class); + setToolSize(500, 400); + ComponentProvider provider = getProvider("Bytes"); showProvider(provider.getClass()); + goToListing(0x400000); captureIsolatedProvider(provider.getClass(), 500, 400); } -@Test - public void testByteViewerOptionsDialog() { + @Test + public void testByteViewerOptionsDialog() { performAction("Byte Viewer Options", "ByteViewerPlugin", false); captureDialog(); - } -@Test - public void testByteViewerExample() { + @Test + public void testByteViewerExample() { AddressSet set = new AddressSet(addr(0x40b000)); ClearCmd cmd = new ClearCmd(set); tool.execute(cmd, program); + + closeProvider(DataTypesProvider.class); + closeProvider(CodeViewerProvider.class); + setToolSize(500, 400); + ComponentProvider provider = getProvider("Bytes"); showProvider(provider.getClass()); + + goToListing(0x41cc08); goToListing(0x40b003); captureIsolatedProvider(provider.getClass(), 500, 400); } -@Test - public void testByteViewerResults() { + @Test + public void testByteViewerResults() { AddressSet set = new AddressSet(addr(0x40b000)); ClearCmd cmd = new ClearCmd(set); tool.execute(cmd, program); - ComponentProvider provider = getProvider("Bytes"); + + closeProvider(DataTypesProvider.class); + closeProvider(CodeViewerProvider.class); + setToolSize(500, 400); + + ByteViewerComponentProvider provider = getProvider(ByteViewerComponentProvider.class); showProvider(provider.getClass()); - performAction("Byte Viewer Options", "ByteViewerPlugin", false); + goToListing(0x41cc08); goToListing(0x40b003); - final DialogComponentProvider dialog = getDialog(); - runSwing(new Runnable() { - @Override - public void run() { - AddressInput addressInput = - (AddressInput) getInstanceField("addressInputField", dialog); - addressInput.setAddress(addr(0x40b003)); - } - }); + runSwing(() -> provider.setOffset(13)); - pressOkOnDialog(); goToListing(0x41cc08); goToListing(0x40b000); goToListing(0x40b003); diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/CharsetsScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/CharsetsScreenShots.java new file mode 100644 index 0000000000..fd0d9f45fe --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/CharsetsScreenShots.java @@ -0,0 +1,37 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package help.screenshot; + +import org.junit.Test; + +import docking.DockingDialog; +import ghidra.util.charset.CharsetInfoManager; +import ghidra.util.charset.picker.CharsetPickerDialog; + +public class CharsetsScreenShots extends GhidraScreenShotGenerator { + + @Test + public void testCharsetPickerDialog() { + + CharsetPickerDialog dlg = new CharsetPickerDialog(); + showDialogWithoutBlocking(tool, dlg); + DockingDialog dialog = (DockingDialog) getInstanceField("dialog", dlg); + runSwing(() -> dialog.setSize(700, 500)); + runSwing(() -> dlg.setSelectedCharset(CharsetInfoManager.getInstance().get("IBM437"))); + + captureDialog(CharsetPickerDialog.class); + } +}