1// Copyright 2019-2020 Graham Clark. All rights reserved.  Use of this source
2// code is governed by the MIT license that can be found in the LICENSE
3// file.
4
5// Package hexdumper2 provides a widget which displays selectable hexdump-like
6// output. Because it's built for termshark, it also allows styling to be
7// applied to ranges of data intended to correspond to packet structure selected
8// in another termshark view.
9package hexdumper2
10
11import (
12	"encoding/hex"
13	"fmt"
14	"io"
15
16	"github.com/gcla/gowid"
17	"github.com/gcla/gowid/gwutil"
18	"github.com/gcla/gowid/widgets/list"
19	"github.com/gcla/gowid/widgets/styled"
20	"github.com/gcla/termshark/v2/format"
21	"github.com/gdamore/tcell"
22)
23
24//======================================================================
25
26type LayerStyler struct {
27	Start         int
28	End           int
29	ColUnselected string
30	ColSelected   string
31}
32
33type PositionChangedCB struct{}
34
35//======================================================================
36
37type Options struct {
38	StyledLayers      []LayerStyler
39	CursorUnselected  string
40	CursorSelected    string
41	LineNumUnselected string
42	LineNumSelected   string
43	PaletteIfCopying  string
44}
45
46type Widget struct {
47	data              []byte
48	layers            []LayerStyler
49	position          int
50	cursorUnselected  string
51	cursorSelected    string
52	lineNumUnselected string
53	lineNumSelected   string
54	offset            int // scroll position, bit of a hack
55	paletteIfCopying  string
56	gowid.AddressProvidesID
57	styled.UsePaletteIfSelectedForCopy
58	Callbacks *gowid.Callbacks
59	gowid.IsSelectable
60}
61
62var _ gowid.IWidget = (*Widget)(nil)
63var _ gowid.IIdentityWidget = (*Widget)(nil)
64
65var _ gowid.IClipboard = (*Widget)(nil)
66var _ gowid.IClipboardSelected = (*Widget)(nil)
67
68func New(data []byte, opts ...Options) *Widget {
69
70	var opt Options
71	if len(opts) > 0 {
72		opt = opts[0]
73	}
74
75	res := &Widget{
76		data:                        data,
77		layers:                      opt.StyledLayers,
78		cursorUnselected:            opt.CursorUnselected,
79		cursorSelected:              opt.CursorSelected,
80		lineNumUnselected:           opt.LineNumUnselected,
81		lineNumSelected:             opt.LineNumSelected,
82		paletteIfCopying:            opt.PaletteIfCopying,
83		UsePaletteIfSelectedForCopy: styled.UsePaletteIfSelectedForCopy{Entry: opt.PaletteIfCopying},
84		Callbacks:                   gowid.NewCallbacks(),
85	}
86
87	return res
88}
89
90func (w *Widget) OnPositionChanged(f gowid.IWidgetChangedCallback) {
91	gowid.AddWidgetCallback(w.Callbacks, PositionChangedCB{}, f)
92}
93
94func (w *Widget) RemoveOnPositionChanged(f gowid.IIdentity) {
95	gowid.RemoveWidgetCallback(w.Callbacks, PositionChangedCB{}, f)
96}
97
98func (w *Widget) String() string {
99	return "hexdump2"
100}
101
102func (w *Widget) CursorUnselected() string {
103	return w.cursorUnselected
104}
105func (w *Widget) CursorSelected() string {
106	return w.cursorSelected
107}
108func (w *Widget) LineNumUnselected() string {
109	return w.lineNumUnselected
110}
111func (w *Widget) LineNumSelected() string {
112	return w.lineNumSelected
113}
114
115func (w *Widget) Layers() []LayerStyler {
116	return w.layers
117}
118
119func (w *Widget) SetLayers(layers []LayerStyler, app gowid.IApp) {
120	w.layers = layers
121}
122
123func (w *Widget) Data() []byte {
124	return w.data
125}
126
127func (w *Widget) SetData(data []byte, app gowid.IApp) {
128	w.data = data
129}
130
131func (w *Widget) Position() int {
132	return w.position
133}
134
135func (w *Widget) SetPosition(pos int, app gowid.IApp) {
136	curpos := w.Position()
137	w.position = pos
138	if curpos != pos {
139		gowid.RunWidgetCallbacks(w.Callbacks, PositionChangedCB{}, app, w)
140	}
141}
142
143func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox {
144	// 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8-->
145	var cols int
146	if sz, ok := size.(gowid.IColumns); ok {
147		cols = sz.Columns()
148	} else {
149		cols = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8
150	}
151
152	var rows int
153	if box, ok := size.(gowid.IRows); ok {
154		rows = box.Rows()
155	} else {
156		rows = (len(w.data) + 15) / 16
157	}
158	return gowid.MakeRenderBox(cols, rows)
159}
160
161// 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8-->
162//  0660   72 6f 72 73 2e 57 69 74  68 53 74 61 63 6b 28 67   rors.Wit hStack(g
163//  0670   6f 77 69 64 2e 57 69 74  68 4b 56 73 28 4f 70 65   owid.Wit hKVs(Ope
164//  0680   6e 45 72 72 6f 72 2c 20  6d 61 70 5b 73 74 72 69   nError,  map[stri
165//  0690   6e 67 5d 69 6e 74 65 72  66 61 63 65 7b 7d 7b 0a   ng]inter face{}{.
166//  06a0   09 09 09 09 22 64 65 73  63 72 69 70 74 6f 72 22   ...."des criptor"
167//  06b0   3a 20 6e 65 77 73 74 64  69 6e 2c 0a 09 09 09 09   : newstd in,.....
168//
169func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas {
170	var canvasRows int
171	if box, ok := size.(gowid.IRows); ok {
172		canvasRows = box.Rows()
173	} else {
174		canvasRows = (len(w.data) + 15) / 16
175	}
176
177	if canvasRows == 0 {
178		return gowid.NewCanvas()
179	}
180
181	// -1 means not copy mode. 0 means the whole hexdump. 2 means the smallest layer, 1 the next biggest
182	diff := -1
183	if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus {
184		diff = app.CopyModeClaimedAt() - app.CopyLevel()
185	}
186
187	cols := 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1 + 8
188	ccols := cols
189	if sz, ok := size.(gowid.IColumns); ok {
190		ccols = gwutil.Max(ccols, sz.Columns())
191	}
192	c := gowid.NewCanvasOfSize(ccols, canvasRows)
193
194	// Adjust in case it's been set too high e.g. via a scrollbar
195	drows := (len(w.data) + 15) / 16
196	if w.offset >= drows {
197		w.offset = drows
198	}
199	rows := gwutil.Min(canvasRows, drows)
200
201	if w.Position() < w.offset*16 {
202		w.SetPosition(w.Position()%16+(w.offset*16), app)
203	}
204
205	if w.Position() >= ((canvasRows + w.offset) * 16) {
206		w.SetPosition(w.Position()%16+((canvasRows+w.offset-1)*16), app)
207	}
208
209	var lineNumStyle convertedStyle
210	var cursorStyle convertedStyle
211	var copyModeStyle convertedStyle
212
213	if focus.Focus {
214		lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumSelected()), app)
215		cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorSelected()), app)
216	} else {
217		lineNumStyle = convertStyle(gowid.MakePaletteRef(w.LineNumUnselected()), app)
218		cursorStyle = convertStyle(gowid.MakePaletteRef(w.CursorUnselected()), app)
219	}
220	copyModeStyle = convertStyle(gowid.MakePaletteRef(w.Entry), app)
221
222	twoByteWriter := CanvasSlice{
223		C: c,
224	}
225
226	asciiWriter := CanvasSlice{
227		C: c,
228	}
229
230	var active *convertedStyle   // for styling the hex data "41" and the ascii "A"
231	var spactive *convertedStyle // for styling the spaces between data e.g. "41 42"
232
233	// nil
234	// [1, 5]
235	// [2, 3]
236	//
237	// Deliberately add a blank layer at the beginning for index 0
238	layers := w.Layers()
239	var layer *LayerStyler
240
241	layerStyles := make([]convertedLayer, len(layers))
242	for i := 0; i < len(layers); i++ {
243		layerStyles[i].u = convertStyle(gowid.MakePaletteRef(layers[i].ColUnselected), app)
244		layerStyles[i].s = convertStyle(gowid.MakePaletteRef(layers[i].ColSelected), app)
245	}
246
247	var i int
248Loop:
249	for row := w.offset; row < rows+w.offset; row++ {
250		twoByteWriter.XOffset = 1 + 4 + 3
251		asciiWriter.XOffset = 1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3
252
253		for col := 0; col < 16; col++ {
254			i = (row * 16) + col
255
256			if i == len(w.data) {
257				break Loop
258			}
259
260			active = nil
261			spactive = nil
262
263			if w.Position() == i {
264				if diff != -1 {
265					active = &copyModeStyle
266				} else {
267					active = &cursorStyle
268				}
269			} else {
270				for j := 0; j < len(layers); j++ {
271					layer = &layers[j]
272					if i >= layer.Start && i < layer.End {
273						if j+1 == diff {
274							active = &copyModeStyle
275							break
276						} else {
277							if focus.Focus {
278								active = &layerStyles[j].s
279							} else {
280								active = &layerStyles[j].u
281							}
282						}
283					}
284				}
285			}
286
287			for j := 0; j < len(layers); j++ {
288				layer = &layers[j]
289				if i >= layer.Start && i < layer.End-1 {
290					if j+1 == diff {
291						spactive = &copyModeStyle
292						break
293					} else {
294						if focus.Focus {
295							spactive = &layerStyles[j].s
296						} else {
297							spactive = &layerStyles[j].u
298						}
299					}
300				}
301			}
302
303			fmt.Fprintf(twoByteWriter, "%02x", w.data[i])
304
305			if active != nil {
306				styleAt(c, twoByteWriter.XOffset, twoByteWriter.YOffset, *active)
307				styleAt(c, twoByteWriter.XOffset+1, twoByteWriter.YOffset, *active)
308			}
309			if spactive != nil {
310				styleAt(c, twoByteWriter.XOffset+2, twoByteWriter.YOffset, *spactive)
311				if col == 7 {
312					styleAt(c, twoByteWriter.XOffset+3, twoByteWriter.YOffset, *spactive)
313				}
314			}
315
316			twoByteWriter.XOffset += 3
317			if col == 7 {
318				twoByteWriter.XOffset += 1
319			}
320
321			ch := '.'
322			r := w.data[i]
323			if r >= 32 && r <= 126 {
324				ch = rune(byte(r))
325			}
326
327			fmt.Fprintf(asciiWriter, "%c", ch)
328
329			if active != nil {
330				styleAt(c, asciiWriter.XOffset, asciiWriter.YOffset, *active)
331			}
332			if spactive != nil && col == 7 {
333				styleAt(c, asciiWriter.XOffset+1, asciiWriter.YOffset, *spactive)
334			}
335
336			asciiWriter.XOffset += 1
337			if col == 7 {
338				asciiWriter.XOffset += 1
339			}
340
341		}
342
343		twoByteWriter.YOffset += 1
344		asciiWriter.YOffset += 1
345	}
346
347	lineNumWriter := CanvasSlice{
348		C:       c,
349		XOffset: 1,
350		YOffset: 0,
351	}
352
353Loop2:
354	for k, rk := w.offset, 0; k < rows+w.offset; k, rk = k+1, rk+1 {
355		if k*16 >= len(w.data) {
356			break Loop2
357		}
358
359		fmt.Fprintf(lineNumWriter, "%04x", k*16)
360
361		lineNumWriter.YOffset += 1
362
363		active := false
364		for _, layer := range layers {
365			if (k+1)*16 >= layer.Start && k*16 < layer.End {
366				active = true
367				break
368			}
369		}
370		if active {
371			for x := 0; x < 6; x++ {
372				styleAt(c, x, rk, lineNumStyle)
373			}
374		}
375	}
376
377	if sz, ok := size.(gowid.IColumns); ok {
378		c.TrimRight(sz.Columns())
379	}
380
381	if diff == 0 {
382		gowid.RangeOverCanvas(c, gowid.CellRangeFunc(func(cell gowid.Cell) gowid.Cell {
383			return cell.WithBackgroundColor(copyModeStyle.b).WithForegroundColor(copyModeStyle.f).WithStyle(copyModeStyle.s)
384		}))
385	}
386
387	return c
388}
389
390func clipsForBytes(data []byte, start int, end int) []gowid.ICopyResult {
391	dump := hex.Dump(data[start:end])
392	dump2 := format.MakeEscapedString(data[start:end])
393	dump3 := format.MakePrintableString(data[start:end])
394	dump4 := format.MakeHexStream(data[start:end])
395
396	return []gowid.ICopyResult{
397		gowid.CopyResult{
398			Name: fmt.Sprintf("Copy bytes %d-%d as hex + ascii", start, end),
399			Val:  dump,
400		},
401		gowid.CopyResult{
402			Name: fmt.Sprintf("Copy bytes %d-%d as escaped string", start, end),
403			Val:  dump2,
404		},
405		gowid.CopyResult{
406			Name: fmt.Sprintf("Copy bytes %d-%d as printable string", start, end),
407			Val:  dump3,
408		},
409		gowid.CopyResult{
410			Name: fmt.Sprintf("Copy bytes %d-%d as hex stream", start, end),
411			Val:  dump4,
412		},
413	}
414}
415
416func (w *Widget) CopyModeLevels() int {
417	return len(w.layers)
418}
419
420func (w *Widget) Clips(app gowid.IApp) []gowid.ICopyResult {
421
422	diff := app.CopyModeClaimedAt() - app.CopyLevel()
423	if diff == 0 {
424		return clipsForBytes(w.Data(), 0, len(w.Data()))
425	} else {
426		return clipsForBytes(w.Data(), w.layers[diff-1].Start, w.layers[diff-1].End)
427	}
428}
429
430func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
431	return gowid.CopyModeUserInput(forCopyModeWidget{forUserInputWidget{Widget: w}}, ev, size, focus, app)
432}
433
434type forCopyModeWidget struct {
435	forUserInputWidget
436}
437
438// CopyModeUserInput calls UserInput() on w.SubWidget() - which is this. Then...
439func (w forCopyModeWidget) SubWidget() gowid.IWidget {
440	return w.forUserInputWidget
441}
442
443type forUserInputWidget struct {
444	*Widget
445}
446
447// ... UserInput is sent to the hexdumper's UserInput logic.
448func (w forUserInputWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
449	return w.Widget.realUserInput(ev, size, focus, app)
450}
451
452// Reject tab because I want it to switch views. Not intended to be transferable.
453func (w *Widget) realUserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
454	res := false
455
456	scrollDown := false
457	scrollUp := false
458
459	switch ev := ev.(type) {
460	case *tcell.EventKey:
461
462		switch ev.Key() {
463		case tcell.KeyRight, tcell.KeyCtrlF:
464			//res = Scroll(w, 1, w.Wrap(), app)
465			pos := w.Position()
466			if pos < len(w.data) {
467				w.SetPosition(pos+1, app)
468				res = true
469			}
470		case tcell.KeyLeft, tcell.KeyCtrlB:
471			pos := w.Position()
472			if pos > 0 {
473				w.SetPosition(pos-1, app)
474				res = true
475			}
476		case tcell.KeyDown, tcell.KeyCtrlN:
477			scrollDown = true
478		case tcell.KeyUp, tcell.KeyCtrlP:
479			scrollUp = true
480		}
481
482	case *tcell.EventMouse:
483		switch ev.Buttons() {
484		case tcell.WheelDown:
485			scrollDown = true
486		case tcell.WheelUp:
487			scrollUp = true
488		case tcell.Button1:
489			xp := -1
490			mx, my := ev.Position()
491			// 1<-4><3><----------(8 * 3)-1---<2<-------(8 * 3)-1-----><3><---8-->1<---8-->
492			//  0660   72 6f 72 73 2e 57 69 74  68 53 74 61 63 6b 28 67   rors.Wit hStack(g
493			//  0670   6f 77 69 64 2e 57 69 74  68 4b 56 73 28 4f 70 65   owid.Wit hKVs(Ope
494			switch {
495			case mx >= 1+4+3 && mx < 1+4+3+((8*3)-1):
496				xp = (mx - (1 + 4 + 3)) / 3
497			case mx >= 1+4+3+((8*3)-1)+2 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1):
498				xp = ((mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2)) / 3) + 8
499			case mx >= 1+4+3+((8*3)-1)+2+((8*3)-1)+3 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8:
500				xp = mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3)
501			case mx >= 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8+1 && mx < 1+4+3+((8*3)-1)+2+((8*3)-1)+3+8+1+8:
502				xp = mx - (1 + 4 + 3 + ((8 * 3) - 1) + 2 + ((8 * 3) - 1) + 3 + 8 + 1) + 8
503			}
504			if xp != -1 {
505				pos := (my * 16) + xp
506				if pos < len(w.data) {
507					w.SetPosition(pos, app)
508					res = true
509				}
510			}
511
512		}
513	}
514
515	pos := w.Position()
516	atBottom := false
517	atTop := false
518
519	var canvasRows int
520	if box, ok := size.(gowid.IRows); ok {
521		canvasRows = box.Rows()
522	} else {
523		canvasRows = (len(w.data) + 15) / 16
524	}
525
526	atTop = ((pos / 16) == w.offset)
527	atBottom = ((pos / 16) == (w.offset + canvasRows - 1))
528
529	if scrollDown {
530		if pos+16 < len(w.data) {
531			w.SetPosition(pos+16, app)
532			if atBottom {
533				w.offset += 1
534			}
535			res = true
536		}
537	} else if scrollUp {
538		if pos-16 >= 0 {
539			w.SetPosition(pos-16, app)
540			if atTop {
541				w.offset -= 1
542			}
543			res = true
544		}
545	}
546
547	return res
548}
549
550// Implement withscrollbar.IScrollValues
551func (t *Widget) ScrollLength() int {
552	return (len(t.data) + 15) / 16
553}
554
555// Implement withscrollbar.IScrollValues
556func (t *Widget) ScrollPosition() int {
557	return t.Position() / 16
558}
559
560// Implements withscrollbar.iSetPosition. Note that position is a row.
561func (t *Widget) SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) {
562	t.offset = pos.ToInt()
563}
564
565// Can leave the cursor out of sight
566func (t *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) {
567	t.offset -= lines
568	if t.offset < 0 {
569		t.offset = 0
570	}
571}
572
573// Adjusts cursor pos too
574func (t *Widget) GoHome(size gowid.IRenderSize, app gowid.IApp) {
575	t.offset = 0
576	t.SetPosition(0, app)
577}
578
579func (t *Widget) GoToEnd(size gowid.IRenderSize, app gowid.IApp) {
580	var canvasRows int
581	if box, ok := size.(gowid.IRows); ok {
582		canvasRows = box.Rows()
583	} else {
584		canvasRows = (len(t.data) + 15) / 16
585	}
586
587	dataRows := (len(t.data) + 15) / 16
588	t.offset += gwutil.Max(0, dataRows-(canvasRows+t.offset))
589
590	t.SetPosition(len(t.data)-1, app)
591}
592
593// Can leave the cursor out of sight
594func (t *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) {
595	var canvasRows int
596	if box, ok := size.(gowid.IRows); ok {
597		canvasRows = box.Rows()
598	} else {
599		canvasRows = (len(t.data) + 15) / 16
600	}
601
602	dataRows := (len(t.data) + 15) / 16
603	t.offset += gwutil.Max(0, gwutil.Min(lines, dataRows-(canvasRows+t.offset)))
604}
605
606func (t *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) {
607	units := 1
608	if size, ok := size.(gowid.IRows); ok {
609		units = size.Rows()
610	}
611
612	for i := 0; i < num; i++ {
613		t.Down(units, size, app)
614	}
615}
616
617func (t *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) {
618	units := 1
619	if size, ok := size.(gowid.IRows); ok {
620		units = size.Rows()
621	}
622
623	for i := 0; i < num; i++ {
624		t.Up(units, size, app)
625	}
626}
627
628//======================================================================
629
630// Optimization - convert the styles for use in the canvas once per call
631// to Render()
632type convertedStyle struct {
633	f gowid.TCellColor
634	b gowid.TCellColor
635	s gowid.StyleAttrs
636}
637
638type convertedLayer struct {
639	u convertedStyle
640	s convertedStyle
641}
642
643func convertStyle(style gowid.ICellStyler, app gowid.IApp) convertedStyle {
644	f, b, s := style.GetStyle(app)
645	f1 := gowid.IColorToTCell(f, gowid.ColorNone, app.GetColorMode())
646	b1 := gowid.IColorToTCell(b, gowid.ColorNone, app.GetColorMode())
647	return convertedStyle{
648		f: f1,
649		b: b1,
650		s: s,
651	}
652}
653
654//======================================================================
655
656type CanvasSlice struct {
657	C       *gowid.Canvas
658	XOffset int
659	YOffset int
660}
661
662var _ io.Writer = CanvasSlice{}
663var _ gowid.IRangeOverCanvas = CanvasSlice{}
664
665func NewCanvasSlice(c *gowid.Canvas, xoff, yoff int) CanvasSlice {
666	res := CanvasSlice{
667		C:       c,
668		XOffset: xoff,
669		YOffset: yoff,
670	}
671	return res
672}
673
674func (c CanvasSlice) Write(p []byte) (n int, err error) {
675	return gowid.WriteToCanvas(c, p)
676}
677
678func (c CanvasSlice) ImplementsWidgetDimension() {}
679
680func (c CanvasSlice) BoxColumns() int {
681	return c.C.BoxColumns()
682}
683
684func (c CanvasSlice) BoxRows() int {
685	return c.C.BoxRows()
686}
687
688func (c CanvasSlice) CellAt(col, row int) gowid.Cell {
689	return c.C.CellAt(col+c.XOffset, row+c.YOffset)
690}
691
692func (c CanvasSlice) SetCellAt(col, row int, cell gowid.Cell) {
693	c.C.SetCellAt(col+c.XOffset, row+c.YOffset, cell)
694}
695
696func styleAt(canvas gowid.ICanvas, col int, row int, st convertedStyle) {
697	c := canvas.CellAt(col, row)
698	c = c.MergeDisplayAttrsUnder(c.WithForegroundColor(st.f).WithBackgroundColor(st.b).WithStyle(st.s))
699	canvas.SetCellAt(col, row, c)
700}
701
702//======================================================================
703// Local Variables:
704// mode: Go
705// fill-column: 110
706// End:
707