panorama-tools 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panorama/__init__.py +0 -0
- panorama/binary.py +577 -0
- panorama/colors.py +7 -0
- panorama/filesystem.py +95 -0
- panorama/main.py +183 -0
- panorama/menu.py +48 -0
- panorama/nodes.py +117 -0
- panorama/plugins/__init__.py +71 -0
- panorama/plugins/aal/bameta.py +104 -0
- panorama/plugins/aal/bars.py +76 -0
- panorama/plugins/aal/barslist.py +53 -0
- panorama/plugins/agl/pmaa.py +76 -0
- panorama/plugins/common/byaml.py +108 -0
- panorama/plugins/image.py +54 -0
- panorama/plugins/lms/msbp.py +109 -0
- panorama/plugins/lms/msbt.py +78 -0
- panorama/plugins/nw/bfwav.py +473 -0
- panorama/plugins/sead/sarc.py +112 -0
- panorama/plugins/sead/yaz0.py +86 -0
- panorama/plugins/zstd.py +71 -0
- panorama/properties.py +52 -0
- panorama/signals.py +16 -0
- panorama/text.py +26 -0
- panorama/utils.py +14 -0
- panorama/widgets.py +20 -0
- panorama/workspace.py +121 -0
- panorama_tools-0.0.2.dist-info/METADATA +58 -0
- panorama_tools-0.0.2.dist-info/RECORD +32 -0
- panorama_tools-0.0.2.dist-info/WHEEL +5 -0
- panorama_tools-0.0.2.dist-info/entry_points.txt +2 -0
- panorama_tools-0.0.2.dist-info/licenses/LICENSE +674 -0
- panorama_tools-0.0.2.dist-info/top_level.txt +1 -0
panorama/__init__.py
ADDED
|
File without changes
|
panorama/binary.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
|
|
2
|
+
from PyQt6.QtCore import *
|
|
3
|
+
from PyQt6.QtGui import *
|
|
4
|
+
from PyQt6.QtWidgets import *
|
|
5
|
+
from panorama import signals
|
|
6
|
+
import html
|
|
7
|
+
import string
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PRINTABLE = string.ascii_letters + string.digits + string.punctuation + " "
|
|
11
|
+
|
|
12
|
+
def formatChar(byte: int) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Returns the character that is shown in the ascii view for a given byte.
|
|
15
|
+
"""
|
|
16
|
+
char = chr(byte)
|
|
17
|
+
if char in PRINTABLE:
|
|
18
|
+
return html.escape(char)
|
|
19
|
+
return "."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TextStream:
|
|
23
|
+
_text: str
|
|
24
|
+
_color: str | None
|
|
25
|
+
_bgcolor: str | None
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self._text = ""
|
|
29
|
+
self._color = None
|
|
30
|
+
self._bgcolor = None
|
|
31
|
+
|
|
32
|
+
def setColor(self, color: str | None) -> None:
|
|
33
|
+
if color != self._color:
|
|
34
|
+
if self._color or self._bgcolor:
|
|
35
|
+
self._text += "</span>"
|
|
36
|
+
self._color = color
|
|
37
|
+
self._applyStyle()
|
|
38
|
+
|
|
39
|
+
def setBackground(self, color: str | None) -> None:
|
|
40
|
+
if color != self._bgcolor:
|
|
41
|
+
if self._color or self._bgcolor:
|
|
42
|
+
self._text += "</span>"
|
|
43
|
+
self._bgcolor = color
|
|
44
|
+
self._applyStyle()
|
|
45
|
+
|
|
46
|
+
def write(self, text: str) -> None:
|
|
47
|
+
self._text += text.replace("\n", "<br>").replace(" ", " ")
|
|
48
|
+
|
|
49
|
+
def get(self) -> str:
|
|
50
|
+
if self._color or self._bgcolor:
|
|
51
|
+
self._text += "</span>"
|
|
52
|
+
self._color = None
|
|
53
|
+
self._bgcolor = None
|
|
54
|
+
return self._text
|
|
55
|
+
|
|
56
|
+
def _applyStyle(self) -> None:
|
|
57
|
+
style = []
|
|
58
|
+
if self._color:
|
|
59
|
+
style.append(f"color: {self._color}")
|
|
60
|
+
if self._bgcolor:
|
|
61
|
+
style.append(f"background-color: {self._bgcolor}")
|
|
62
|
+
|
|
63
|
+
if style:
|
|
64
|
+
self._text += f"<span style='{"; ".join(style)}'>"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BinaryContextMenu(QMenu):
|
|
68
|
+
copy: QAction
|
|
69
|
+
selectAll: QAction
|
|
70
|
+
|
|
71
|
+
def __init__(self, parent: QWidget | None = None):
|
|
72
|
+
super().__init__(parent)
|
|
73
|
+
self.copy = QAction("Copy")
|
|
74
|
+
self.copy.setIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditCopy))
|
|
75
|
+
self.copy.setShortcut(QKeySequence.StandardKey.Copy)
|
|
76
|
+
|
|
77
|
+
self.selectAll = QAction("Select All")
|
|
78
|
+
self.selectAll.setIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditSelectAll))
|
|
79
|
+
self.selectAll.setShortcut(QKeySequence.StandardKey.SelectAll)
|
|
80
|
+
|
|
81
|
+
self.addAction(self.copy)
|
|
82
|
+
self.addSeparator()
|
|
83
|
+
self.addAction(self.selectAll)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BinaryView(QTextEdit):
|
|
87
|
+
viewChanged: signals.Signal
|
|
88
|
+
"""Emitted when the size of the view changes."""
|
|
89
|
+
|
|
90
|
+
wheel: signals.Signal[QWheelEvent]
|
|
91
|
+
"""Emitted when a scroll wheel event occurs on the view."""
|
|
92
|
+
|
|
93
|
+
scrollRequest: signals.Signal[int]
|
|
94
|
+
"""
|
|
95
|
+
Emitted when the view should scroll by the given number of rows, either due
|
|
96
|
+
to mouse of keyboard events.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
jump: signals.Signal[int]
|
|
100
|
+
"""Emitted when the view should jump to the given offset in the file."""
|
|
101
|
+
|
|
102
|
+
_data: bytes
|
|
103
|
+
"""Content of the file that is shown in the binary view."""
|
|
104
|
+
|
|
105
|
+
_base: int
|
|
106
|
+
"""Current base address of the view."""
|
|
107
|
+
|
|
108
|
+
_rows: int
|
|
109
|
+
"""Number of rows that are currently displayed."""
|
|
110
|
+
|
|
111
|
+
_end: int
|
|
112
|
+
"""End address of the range that is currently displayed."""
|
|
113
|
+
|
|
114
|
+
_cursorAddr: int
|
|
115
|
+
"""Address of the cursor."""
|
|
116
|
+
|
|
117
|
+
_cursorOffset: int
|
|
118
|
+
"""Position of the cursor in the current byte."""
|
|
119
|
+
|
|
120
|
+
_cursorAscii: bool
|
|
121
|
+
"""Whether the cursor is active in the hex or ascii view."""
|
|
122
|
+
|
|
123
|
+
_selectionAnchor: int
|
|
124
|
+
"""The address at which the user began making a selection."""
|
|
125
|
+
|
|
126
|
+
_selectionStart: int
|
|
127
|
+
"""The lowest address of the selection."""
|
|
128
|
+
|
|
129
|
+
_selectionEnd: int
|
|
130
|
+
"""The highest address of the selection."""
|
|
131
|
+
|
|
132
|
+
_selectionAscii: bool
|
|
133
|
+
"""Whether the user is making a selection in the hex or ascii view."""
|
|
134
|
+
|
|
135
|
+
_mousePos: int
|
|
136
|
+
"""
|
|
137
|
+
Current position of the mouse relative to the top of the view, while the
|
|
138
|
+
user is dragging a selection. This is used to determine how fast the view
|
|
139
|
+
should scroll when the user moves the mouse outside of the view.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
_timer: QTimer
|
|
143
|
+
"""
|
|
144
|
+
Timer that periodically scrolls the view while the user drags a selection
|
|
145
|
+
outside of the view.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, data: bytes):
|
|
149
|
+
super().__init__()
|
|
150
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
151
|
+
self.verticalScrollBar().valueChanged.connect(self._resetScroll)
|
|
152
|
+
|
|
153
|
+
self._data = data
|
|
154
|
+
|
|
155
|
+
self.viewChanged = signals.Signal()
|
|
156
|
+
self.wheel = signals.Signal()
|
|
157
|
+
self.scrollRequest = signals.Signal()
|
|
158
|
+
self.jump = signals.Signal()
|
|
159
|
+
|
|
160
|
+
self._base = 0
|
|
161
|
+
self._rows = 0
|
|
162
|
+
self._end = 0
|
|
163
|
+
|
|
164
|
+
self._cursorAddr = 0
|
|
165
|
+
self._cursorOffset = 0
|
|
166
|
+
self._cursorAscii = False
|
|
167
|
+
|
|
168
|
+
self._selectionAnchor = 0
|
|
169
|
+
self._selectionStart = 0
|
|
170
|
+
self._selectionEnd = 0
|
|
171
|
+
self._selectionAscii = False
|
|
172
|
+
|
|
173
|
+
self._mousePos = 0
|
|
174
|
+
|
|
175
|
+
self._timer = QTimer()
|
|
176
|
+
self._timer.timeout.connect(self._updateScroll)
|
|
177
|
+
self._timer.setInterval(50)
|
|
178
|
+
self._timer.start()
|
|
179
|
+
|
|
180
|
+
def setRange(self, offset: int, rows: int) -> None:
|
|
181
|
+
self._base = offset
|
|
182
|
+
self._rows = rows
|
|
183
|
+
self._end = min(offset + rows * 16, len(self._data) + 1)
|
|
184
|
+
self._updateText()
|
|
185
|
+
|
|
186
|
+
def resizeEvent(self, e: QResizeEvent) -> None:
|
|
187
|
+
super().resizeEvent(e)
|
|
188
|
+
self.viewChanged.emit()
|
|
189
|
+
|
|
190
|
+
def wheelEvent(self, e: QWheelEvent) -> None:
|
|
191
|
+
self.wheel.emit(e)
|
|
192
|
+
|
|
193
|
+
def contextMenuEvent(self, e: QContextMenuEvent) -> None:
|
|
194
|
+
menu = BinaryContextMenu(self)
|
|
195
|
+
menu.copy.setEnabled(self._selectionEnd > self._selectionStart)
|
|
196
|
+
|
|
197
|
+
menu.copy.triggered.connect(self._handleCopy)
|
|
198
|
+
menu.selectAll.triggered.connect(self._handleSelectAll)
|
|
199
|
+
|
|
200
|
+
menu.exec(e.globalPos())
|
|
201
|
+
|
|
202
|
+
def keyPressEvent(self, e: QKeyEvent) -> None:
|
|
203
|
+
self._handleKeyPress(e)
|
|
204
|
+
self._updateText()
|
|
205
|
+
|
|
206
|
+
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
|
|
207
|
+
self.mousePressEvent(e)
|
|
208
|
+
|
|
209
|
+
def mousePressEvent(self, e: QMouseEvent) -> None:
|
|
210
|
+
if e.button() == Qt.MouseButton.LeftButton:
|
|
211
|
+
cursor = self.cursorForPosition(e.pos())
|
|
212
|
+
self._cursorAddr, self._cursorOffset, self._cursorAscii = \
|
|
213
|
+
self._getCursorInfo(cursor.position())
|
|
214
|
+
|
|
215
|
+
self._clampCursor()
|
|
216
|
+
|
|
217
|
+
self._selectionAnchor = self._selectionAddr(
|
|
218
|
+
self._cursorAddr, self._cursorOffset, self._cursorAscii
|
|
219
|
+
)
|
|
220
|
+
self._selectionStart = self._selectionAnchor
|
|
221
|
+
self._selectionEnd = self._selectionAnchor
|
|
222
|
+
self._selectionAscii = self._cursorAscii
|
|
223
|
+
|
|
224
|
+
self._updateText()
|
|
225
|
+
|
|
226
|
+
def mouseMoveEvent(self, e):
|
|
227
|
+
if e.buttons() & Qt.MouseButton.LeftButton:
|
|
228
|
+
self._mousePos = e.y()
|
|
229
|
+
|
|
230
|
+
cursor = self.cursorForPosition(e.pos())
|
|
231
|
+
self._cursorAddr, self._cursorOffset, self._cursorAscii = \
|
|
232
|
+
self._getCursorInfo(cursor.position(), self._cursorAscii)
|
|
233
|
+
|
|
234
|
+
self._clampCursor()
|
|
235
|
+
|
|
236
|
+
selectionPos = self._selectionAddr(
|
|
237
|
+
self._cursorAddr, self._cursorOffset, self._cursorAscii
|
|
238
|
+
)
|
|
239
|
+
self._moveSelectionTo(selectionPos)
|
|
240
|
+
|
|
241
|
+
self._updateText()
|
|
242
|
+
|
|
243
|
+
def mouseReleaseEvent(self, e):
|
|
244
|
+
if e.button() == Qt.MouseButton.LeftButton:
|
|
245
|
+
self._mousePos = 0
|
|
246
|
+
|
|
247
|
+
def _resetScroll(self) -> None:
|
|
248
|
+
"""Disables functionality of the scroll bar."""
|
|
249
|
+
self.verticalScrollBar().setValue(0)
|
|
250
|
+
|
|
251
|
+
def _updateScroll(self) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Called every 50 milliseconds. This scrolls the view while the user drags
|
|
254
|
+
a selection outside of the view.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
if self._mousePos < 0:
|
|
258
|
+
self.scrollRequest.emit(self._mousePos // 10)
|
|
259
|
+
elif self._mousePos > self.height():
|
|
260
|
+
self.scrollRequest.emit((self._mousePos - self.height()) // 10)
|
|
261
|
+
|
|
262
|
+
def _handleKeyPress(self, e: QKeyEvent) -> None:
|
|
263
|
+
self._checkKeySequence(e)
|
|
264
|
+
self._checkNavigationKeys(e)
|
|
265
|
+
|
|
266
|
+
def _checkKeySequence(self, e: QKeyEvent) -> None:
|
|
267
|
+
if e.matches(QKeySequence.StandardKey.Copy):
|
|
268
|
+
self._handleCopy()
|
|
269
|
+
elif e.matches(QKeySequence.StandardKey.SelectAll):
|
|
270
|
+
self._handleSelectAll()
|
|
271
|
+
|
|
272
|
+
def _checkNavigationKeys(self, e: QKeyEvent) -> None:
|
|
273
|
+
key = e.key()
|
|
274
|
+
|
|
275
|
+
if key == Qt.Key.Key_Left:
|
|
276
|
+
if self._cursorOffset != 0:
|
|
277
|
+
self._cursorOffset = 0
|
|
278
|
+
elif self._cursorAddr > 0:
|
|
279
|
+
self._cursorAddr -= 1
|
|
280
|
+
|
|
281
|
+
elif key == Qt.Key.Key_Right:
|
|
282
|
+
self._cursorOffset = 0
|
|
283
|
+
if self._cursorAddr < len(self._data):
|
|
284
|
+
self._cursorAddr += 1
|
|
285
|
+
|
|
286
|
+
elif key == Qt.Key.Key_Up:
|
|
287
|
+
if self._cursorAddr >= 16:
|
|
288
|
+
self._cursorAddr -= 16
|
|
289
|
+
|
|
290
|
+
elif key == Qt.Key.Key_Down:
|
|
291
|
+
if self._cursorAddr <= len(self._data) - 16:
|
|
292
|
+
self._cursorAddr += 16
|
|
293
|
+
|
|
294
|
+
elif key == Qt.Key.Key_PageUp:
|
|
295
|
+
maxRows = self._cursorAddr // 16
|
|
296
|
+
self._cursorAddr -= min(maxRows, self._rows) * 16
|
|
297
|
+
self.scrollRequest.emit(-self._rows)
|
|
298
|
+
|
|
299
|
+
elif key == Qt.Key.Key_PageDown:
|
|
300
|
+
maxRows = (len(self._data) - self._cursorAddr) // 16
|
|
301
|
+
self._cursorAddr += min(maxRows, self._rows) * 16
|
|
302
|
+
self.scrollRequest.emit(self._rows)
|
|
303
|
+
|
|
304
|
+
elif key == Qt.Key.Key_Home:
|
|
305
|
+
self._cursorAddr &= ~15
|
|
306
|
+
self._cursorOffset = 0
|
|
307
|
+
|
|
308
|
+
elif key == Qt.Key.Key_End:
|
|
309
|
+
self._cursorAddr = ((self._cursorAddr + 16) & ~15)
|
|
310
|
+
|
|
311
|
+
self._cursorOffset = 0
|
|
312
|
+
if self._cursorAddr % 16 == 0 and self._cursorAddr != 0:
|
|
313
|
+
self._cursorAddr -= 1
|
|
314
|
+
self._cursorOffset = 2 - self._cursorAscii
|
|
315
|
+
|
|
316
|
+
if key in [
|
|
317
|
+
Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
|
|
318
|
+
Qt.Key.Key_PageUp, Qt.Key.Key_PageDown, Qt.Key.Key_Home,
|
|
319
|
+
Qt.Key.Key_End
|
|
320
|
+
]:
|
|
321
|
+
modifiers = e.modifiers()
|
|
322
|
+
shift = bool(modifiers & Qt.KeyboardModifier.ShiftModifier)
|
|
323
|
+
|
|
324
|
+
self._clampCursor()
|
|
325
|
+
|
|
326
|
+
if shift:
|
|
327
|
+
selectionPos = self._selectionAddr(
|
|
328
|
+
self._cursorAddr, self._cursorOffset, self._cursorAscii
|
|
329
|
+
)
|
|
330
|
+
self._moveSelectionTo(selectionPos)
|
|
331
|
+
else:
|
|
332
|
+
self._clearSelection()
|
|
333
|
+
|
|
334
|
+
self._jumpToCursor()
|
|
335
|
+
|
|
336
|
+
def _handleCopy(self) -> None:
|
|
337
|
+
data = self._data[self._selectionStart : self._selectionEnd]
|
|
338
|
+
if not data:
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
if self._selectionAscii:
|
|
342
|
+
text = data.decode("charmap")
|
|
343
|
+
else:
|
|
344
|
+
text = data.hex()
|
|
345
|
+
|
|
346
|
+
clipboard = QGuiApplication.clipboard()
|
|
347
|
+
clipboard.setText(text)
|
|
348
|
+
|
|
349
|
+
def _handleSelectAll(self) -> None:
|
|
350
|
+
self._cursorAddr = len(self._data)
|
|
351
|
+
self._cursorOffset = 0
|
|
352
|
+
|
|
353
|
+
self._selectionStart = 0
|
|
354
|
+
self._selectionAnchor = 0
|
|
355
|
+
self._selectionEnd = len(self._data)
|
|
356
|
+
|
|
357
|
+
self._clampCursor()
|
|
358
|
+
self._updateText()
|
|
359
|
+
self._jumpToCursor()
|
|
360
|
+
|
|
361
|
+
def _jumpToCursor(self) -> None:
|
|
362
|
+
if self._cursorAddr < self._base:
|
|
363
|
+
self.jump.emit(self._cursorAddr & ~0xF)
|
|
364
|
+
elif self._cursorAddr >= self._end:
|
|
365
|
+
addr = (self._cursorAddr & ~0xF) - (self._rows - 1) * 16
|
|
366
|
+
self.jump.emit(addr)
|
|
367
|
+
|
|
368
|
+
def _clearSelection(self) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Clears the selection, e.g. when the user presses an arrow key without
|
|
371
|
+
holding shift.
|
|
372
|
+
"""
|
|
373
|
+
addr = self._selectionAddr(
|
|
374
|
+
self._cursorAddr, self._cursorOffset, self._cursorAscii
|
|
375
|
+
)
|
|
376
|
+
self._selectionAnchor = addr
|
|
377
|
+
self._selectionStart = addr
|
|
378
|
+
self._selectionEnd = addr
|
|
379
|
+
|
|
380
|
+
def _moveSelectionTo(self, addr):
|
|
381
|
+
if addr < self._selectionAnchor:
|
|
382
|
+
self._selectionStart = addr
|
|
383
|
+
self._selectionEnd = self._selectionAnchor
|
|
384
|
+
else:
|
|
385
|
+
self._selectionStart = self._selectionAnchor
|
|
386
|
+
self._selectionEnd = addr
|
|
387
|
+
|
|
388
|
+
def _selectionAddr(self, pos, offset, view):
|
|
389
|
+
if pos % 16 == 15 or pos == len(self._data) - 1:
|
|
390
|
+
if view and offset:
|
|
391
|
+
return pos + 1
|
|
392
|
+
return pos + (offset == 2)
|
|
393
|
+
return pos
|
|
394
|
+
|
|
395
|
+
def _clampCursor(self):
|
|
396
|
+
if self._cursorAddr > len(self._data):
|
|
397
|
+
self._cursorAddr = len(self._data)
|
|
398
|
+
|
|
399
|
+
if self._cursorAddr == len(self._data):
|
|
400
|
+
self._cursorOffset = 0
|
|
401
|
+
if self._cursorAddr != 0:
|
|
402
|
+
self._cursorAddr -= 1
|
|
403
|
+
if self._cursorAscii:
|
|
404
|
+
self._cursorOffset = 1
|
|
405
|
+
else:
|
|
406
|
+
self._cursorOffset = 2
|
|
407
|
+
|
|
408
|
+
def _getCursorInfo(
|
|
409
|
+
self, pos: int, ascii: bool | None = None
|
|
410
|
+
) -> tuple[int, int, bool]:
|
|
411
|
+
"""
|
|
412
|
+
From the given cursor position in the text, computes the address of the
|
|
413
|
+
byte under the cursor, the location of the cursor within that byte, and
|
|
414
|
+
whether the cursor is in the hex or ascii view.
|
|
415
|
+
|
|
416
|
+
If the ascii parameter is provided and not None, it forces the cursor to
|
|
417
|
+
be either in the hex or ascii view.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
row = max(pos // 76, 2)
|
|
421
|
+
col = max(pos % 76, 10)
|
|
422
|
+
|
|
423
|
+
if ascii is not None:
|
|
424
|
+
if ascii:
|
|
425
|
+
col = max(col, 59)
|
|
426
|
+
else:
|
|
427
|
+
col = min(col, 57)
|
|
428
|
+
|
|
429
|
+
base = self._base + (row - 2) * 16
|
|
430
|
+
if col < 57:
|
|
431
|
+
return base + (col - 9) // 3, col % 3 == 2, False
|
|
432
|
+
if col == 57:
|
|
433
|
+
return base + 15, 2, False
|
|
434
|
+
if col == 58:
|
|
435
|
+
return base, 0, True
|
|
436
|
+
if col < 75:
|
|
437
|
+
return base + col - 59, 0, True
|
|
438
|
+
return base + 15, 1, True
|
|
439
|
+
|
|
440
|
+
def _getCursorPos(self, addr: int, offset: int, ascii: bool) -> int:
|
|
441
|
+
"""
|
|
442
|
+
Computes the position for a cursor in the text that corresponds to the
|
|
443
|
+
given byte address, cursor offset within that byte, and view.
|
|
444
|
+
"""
|
|
445
|
+
row = 76 * ((addr - self._base) // 16 + 2)
|
|
446
|
+
if ascii:
|
|
447
|
+
return row + 59 + addr % 16 + offset
|
|
448
|
+
return row + 10 + 3 * (addr % 16) + offset
|
|
449
|
+
|
|
450
|
+
def _updateCursor(self) -> None:
|
|
451
|
+
if self._cursorAddr >= self._base and self._cursorAddr < self._end:
|
|
452
|
+
pos = self._getCursorPos(
|
|
453
|
+
self._cursorAddr, self._cursorOffset, self._cursorAscii
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
cursor = QTextCursor(self.document())
|
|
457
|
+
cursor.setPosition(pos)
|
|
458
|
+
self.setTextCursor(cursor)
|
|
459
|
+
else:
|
|
460
|
+
self.setTextCursor(QTextCursor())
|
|
461
|
+
|
|
462
|
+
def _updateColor(self, stream: TextStream, addr: int, ascii: bool) -> None:
|
|
463
|
+
if self._selectionStart <= addr < self._selectionEnd:
|
|
464
|
+
if ascii == self._selectionAscii:
|
|
465
|
+
stream.setBackground("blue")
|
|
466
|
+
else:
|
|
467
|
+
stream.setBackground("lightblue")
|
|
468
|
+
stream.setColor("white")
|
|
469
|
+
|
|
470
|
+
else:
|
|
471
|
+
if not ascii and addr % 2:
|
|
472
|
+
stream.setColor("gray")
|
|
473
|
+
else:
|
|
474
|
+
stream.setColor(None)
|
|
475
|
+
stream.setBackground(None)
|
|
476
|
+
|
|
477
|
+
def _updateText(self) -> None:
|
|
478
|
+
stream = TextStream()
|
|
479
|
+
stream.setColor("blue")
|
|
480
|
+
stream.write("Binary ")
|
|
481
|
+
stream.write("00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
|
|
482
|
+
stream.write(" ")
|
|
483
|
+
stream.write("0123456789ABCDEF\n")
|
|
484
|
+
stream.write(" " * 75 + "\n")
|
|
485
|
+
|
|
486
|
+
addr = self._base
|
|
487
|
+
data = self._data[self._base : self._end]
|
|
488
|
+
while data:
|
|
489
|
+
stream.setColor("blue")
|
|
490
|
+
stream.write(f"{addr:08X} ")
|
|
491
|
+
|
|
492
|
+
for i in range(min(len(data), 16)):
|
|
493
|
+
stream.write(" ")
|
|
494
|
+
self._updateColor(stream, addr + i, False)
|
|
495
|
+
stream.write(f"{data[i]:02X}")
|
|
496
|
+
|
|
497
|
+
stream.setColor(None)
|
|
498
|
+
stream.setBackground(None)
|
|
499
|
+
stream.write(" " * (2 + (15 - i) * 3))
|
|
500
|
+
|
|
501
|
+
for i, byte in enumerate(data[:16]):
|
|
502
|
+
self._updateColor(stream, addr + i, True)
|
|
503
|
+
stream.write(formatChar(byte))
|
|
504
|
+
|
|
505
|
+
stream.setColor(None)
|
|
506
|
+
stream.setBackground(None)
|
|
507
|
+
stream.write("\n")
|
|
508
|
+
|
|
509
|
+
data = data[16:]
|
|
510
|
+
addr += 16
|
|
511
|
+
|
|
512
|
+
if not self._data:
|
|
513
|
+
stream.setColor("blue")
|
|
514
|
+
stream.write("00000000 " + " " * 50)
|
|
515
|
+
|
|
516
|
+
self.setText(stream.get())
|
|
517
|
+
|
|
518
|
+
self._updateCursor()
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class BinaryWidget(QWidget):
|
|
522
|
+
_scrollBar: QScrollBar
|
|
523
|
+
_font: QFont
|
|
524
|
+
_metrics: QFontMetrics
|
|
525
|
+
_view: BinaryView
|
|
526
|
+
_layout: QHBoxLayout
|
|
527
|
+
|
|
528
|
+
def __init__(self, data):
|
|
529
|
+
super().__init__()
|
|
530
|
+
self._data = data
|
|
531
|
+
|
|
532
|
+
self._scrollBar = QScrollBar()
|
|
533
|
+
self._scrollBar.setRange(0, len(data) // 16)
|
|
534
|
+
self._scrollBar.valueChanged.connect(self._updateView)
|
|
535
|
+
|
|
536
|
+
self._font = \
|
|
537
|
+
QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
|
|
538
|
+
self._font.setPixelSize(14)
|
|
539
|
+
self.setFont(self._font)
|
|
540
|
+
|
|
541
|
+
self._metrics = QFontMetrics(self._font)
|
|
542
|
+
|
|
543
|
+
width = self._metrics.boundingRect("_" * 75).width() + 10
|
|
544
|
+
|
|
545
|
+
self._view = BinaryView(data)
|
|
546
|
+
self._view.setMinimumWidth(width)
|
|
547
|
+
self._view.viewChanged.connect(self._updateView)
|
|
548
|
+
self._view.wheel.connect(self._scrollBar.wheelEvent)
|
|
549
|
+
self._view.scrollRequest.connect(self._moveScroll)
|
|
550
|
+
self._view.jump.connect(self._handleJump)
|
|
551
|
+
|
|
552
|
+
self._layout = QHBoxLayout(self)
|
|
553
|
+
self._layout.addWidget(self._view)
|
|
554
|
+
self._layout.addWidget(self._scrollBar)
|
|
555
|
+
|
|
556
|
+
self._updateView()
|
|
557
|
+
|
|
558
|
+
def _moveScroll(self, offset: int) -> None:
|
|
559
|
+
self._scrollBar.setValue(self._scrollBar.value() + offset)
|
|
560
|
+
self._updateView()
|
|
561
|
+
|
|
562
|
+
def _handleJump(self, address: int) -> None:
|
|
563
|
+
rows = int(
|
|
564
|
+
(self._view.height() - 10) / (self._metrics.lineSpacing() + 1)
|
|
565
|
+
)
|
|
566
|
+
rows -= 2
|
|
567
|
+
|
|
568
|
+
self._view.setRange(address, rows)
|
|
569
|
+
|
|
570
|
+
scroll = max((len(self._data) + 15) // 16 - rows, 0)
|
|
571
|
+
self._scrollBar.setVisible(scroll != 0)
|
|
572
|
+
self._scrollBar.setRange(0, scroll)
|
|
573
|
+
self._scrollBar.setValue(address // 16)
|
|
574
|
+
|
|
575
|
+
def _updateView(self) -> None:
|
|
576
|
+
offset = self._scrollBar.value() * 16
|
|
577
|
+
self._handleJump(offset)
|
panorama/colors.py
ADDED
panorama/filesystem.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Provides nodes for files and folders that are stored on disk.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtWidgets import *
|
|
7
|
+
from panorama import nodes, plugins
|
|
8
|
+
import io
|
|
9
|
+
import mmap
|
|
10
|
+
import os
|
|
11
|
+
import qtawesome
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileReader(nodes.Reader):
|
|
15
|
+
"""Reader for files that are stored on disk."""
|
|
16
|
+
|
|
17
|
+
_path: str
|
|
18
|
+
|
|
19
|
+
def __init__(self, path: str):
|
|
20
|
+
self._path = path
|
|
21
|
+
|
|
22
|
+
def path(self) -> str:
|
|
23
|
+
return self._path
|
|
24
|
+
|
|
25
|
+
def filename(self) -> str:
|
|
26
|
+
return os.path.basename(self._path)
|
|
27
|
+
|
|
28
|
+
def read(self) -> bytes | mmap.mmap:
|
|
29
|
+
"""
|
|
30
|
+
Opens the file. Files that are smaller than 1 MB are read completely
|
|
31
|
+
into memory. Larger files are mapped with mmap.
|
|
32
|
+
"""
|
|
33
|
+
with open(self._path, "rb") as f:
|
|
34
|
+
f.seek(0, io.SEEK_END)
|
|
35
|
+
size = f.tell()
|
|
36
|
+
f.seek(0)
|
|
37
|
+
|
|
38
|
+
if size < 1024 * 1024:
|
|
39
|
+
return f.read()
|
|
40
|
+
return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Folder(nodes.Node):
|
|
44
|
+
"""Represents a folder on disk."""
|
|
45
|
+
|
|
46
|
+
_plugins: plugins.Plugins
|
|
47
|
+
_path: str
|
|
48
|
+
|
|
49
|
+
def __init__(self, plugins: plugins.Plugins, path: str):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self._plugins = plugins
|
|
52
|
+
self._path = path
|
|
53
|
+
|
|
54
|
+
self.setText(0, os.path.basename(path))
|
|
55
|
+
self.setIcon(0, qtawesome.icon("fa5s.folder", color="#fc0"))
|
|
56
|
+
|
|
57
|
+
self.showIndicator(bool(os.listdir(self._path)))
|
|
58
|
+
|
|
59
|
+
def path(self) -> str:
|
|
60
|
+
return self._path
|
|
61
|
+
|
|
62
|
+
def priority(self) -> int:
|
|
63
|
+
return -1 # Show folders before everything else
|
|
64
|
+
|
|
65
|
+
def createChildren(self) -> None:
|
|
66
|
+
for name in os.listdir(self._path):
|
|
67
|
+
childpath = os.path.join(self._path, name)
|
|
68
|
+
if os.path.isdir(childpath):
|
|
69
|
+
self.addChild(Folder(self._plugins, childpath))
|
|
70
|
+
elif os.path.isfile(childpath):
|
|
71
|
+
reader = FileReader(childpath)
|
|
72
|
+
self.addChild(self._plugins.create(reader))
|
|
73
|
+
else:
|
|
74
|
+
self.addChild(Other(childpath))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Other(nodes.Node):
|
|
78
|
+
"""
|
|
79
|
+
Special filesystem node.
|
|
80
|
+
|
|
81
|
+
Represents a filesystem node that is neither a folder nor a regular file.
|
|
82
|
+
For example, this could be a symbolic link or a Unix socket file.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
_path: str
|
|
86
|
+
|
|
87
|
+
def __init__(self, path: str):
|
|
88
|
+
super().__init__()
|
|
89
|
+
self._path = path
|
|
90
|
+
|
|
91
|
+
self.setText(0, os.path.basename(path))
|
|
92
|
+
self.setIcon(0, qtawesome.icon("fa5s.question"))
|
|
93
|
+
|
|
94
|
+
def path(self) -> str:
|
|
95
|
+
return self._path
|