160 lines
4.6 KiB
Go
160 lines
4.6 KiB
Go
package tui
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/bubbles/textarea"
|
||
"github.com/charmbracelet/bubbles/viewport"
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
|
||
"github.com/r3g1tpr0cs/burterm/internal/sequencer"
|
||
)
|
||
|
||
// sequencerModel — вкладка Sequencer: textarea с токенами (по одному на
|
||
// строку), запуск FIPS-анализа по ctrl+r и читаемый отчёт по каждому
|
||
// проанализированному 20 000-битному блоку.
|
||
type sequencerModel struct {
|
||
input textarea.Model
|
||
output viewport.Model
|
||
|
||
focus int // 0 — вход, 1 — выход (скролл)
|
||
errMsg string
|
||
}
|
||
|
||
func newSequencerView() sequencerModel {
|
||
ta := textarea.New()
|
||
ta.Placeholder = "Токены по одному на строку — либо hex-строки, либо сырой текст"
|
||
ta.ShowLineNumbers = true
|
||
ta.Focus()
|
||
|
||
return sequencerModel{
|
||
input: ta,
|
||
output: viewport.New(0, 0),
|
||
}
|
||
}
|
||
|
||
func (m *sequencerModel) SetSize(width, height int) {
|
||
if width < 0 {
|
||
width = 0
|
||
}
|
||
if height < 0 {
|
||
height = 0
|
||
}
|
||
inHeight := height / 2
|
||
|
||
m.input.SetWidth(width)
|
||
m.input.SetHeight(inHeight)
|
||
m.output.Width = width
|
||
// 6 фиксированных строк вокруг output-viewport (построчный подсчёт по
|
||
// View(): "Токены"-метка 1, разделитель 1, "Отчёт FIPS"-метка 1,
|
||
// разделитель+футер(две строки) 3 — итого 6). Раньше стояло -4 — не
|
||
// совпадало с View() ещё до того, как футер стал двухстрочным.
|
||
m.output.Height = height - inHeight - 6
|
||
}
|
||
|
||
func (m sequencerModel) Update(msg tea.Msg) (sequencerModel, tea.Cmd) {
|
||
if key, ok := msg.(tea.KeyMsg); ok {
|
||
switch key.String() {
|
||
case "tab":
|
||
m.focus = (m.focus + 1) % 2
|
||
if m.focus == 0 {
|
||
m.input.Focus()
|
||
} else {
|
||
m.input.Blur()
|
||
}
|
||
return m, nil
|
||
case "ctrl+r":
|
||
m.analyze()
|
||
return m, nil
|
||
}
|
||
}
|
||
|
||
var cmd tea.Cmd
|
||
if m.focus == 0 {
|
||
m.input, cmd = m.input.Update(msg)
|
||
} else {
|
||
m.output, cmd = m.output.Update(msg)
|
||
}
|
||
return m, cmd
|
||
}
|
||
|
||
func (m *sequencerModel) analyze() {
|
||
var samples [][]byte
|
||
for _, line := range strings.Split(m.input.Value(), "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
continue
|
||
}
|
||
samples = append(samples, sequencer.ParseSample(line))
|
||
}
|
||
if len(samples) == 0 {
|
||
m.errMsg = "нет сэмплов — вставь токены, по одному на строку"
|
||
return
|
||
}
|
||
|
||
results, blocks, bitsAvailable, err := sequencer.AnalyzeConcatenated(samples)
|
||
if err != nil {
|
||
m.errMsg = err.Error()
|
||
m.output.SetContent(fmt.Sprintf("Сэмплов: %d, доступно бит: %d — недостаточно для анализа.", len(samples), bitsAvailable))
|
||
return
|
||
}
|
||
|
||
m.errMsg = ""
|
||
m.output.SetContent(renderSequencerReport(samples, results, blocks, bitsAvailable))
|
||
}
|
||
|
||
func renderSequencerReport(samples [][]byte, results []sequencer.BlockResult, blocks, bitsAvailable int) string {
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("Сэмплов: %d · доступно бит: %d · проанализировано блоков по 20000 бит: %d\n\n", len(samples), bitsAvailable, blocks))
|
||
|
||
passCount := 0
|
||
for _, r := range results {
|
||
if r.AllPass() {
|
||
passCount++
|
||
}
|
||
}
|
||
b.WriteString(fmt.Sprintf("Итог: %d/%d блоков прошли все четыре теста\n\n", passCount, blocks))
|
||
|
||
for i, r := range results {
|
||
b.WriteString(fmt.Sprintf("Блок %d:\n", i+1))
|
||
b.WriteString(fmt.Sprintf(" Monobit: единиц=%d %s\n", r.MonobitOnes, passLabel(r.MonobitPass)))
|
||
b.WriteString(fmt.Sprintf(" Poker: X=%.2f %s\n", r.PokerX, passLabel(r.PokerPass)))
|
||
b.WriteString(fmt.Sprintf(" Runs: %s\n", passLabel(r.RunsPass)))
|
||
b.WriteString(fmt.Sprintf(" Long run: макс. серия=%d бит %s\n", r.LongRunMax, passLabel(r.LongRunPass)))
|
||
b.WriteString("\n")
|
||
}
|
||
|
||
return b.String()
|
||
}
|
||
|
||
func passLabel(pass bool) string {
|
||
if pass {
|
||
return "PASS"
|
||
}
|
||
return "FAIL"
|
||
}
|
||
|
||
func (m sequencerModel) View() string {
|
||
focusMark := func(i int) string {
|
||
if m.focus == i {
|
||
return "▸ "
|
||
}
|
||
return " "
|
||
}
|
||
|
||
var b strings.Builder
|
||
b.WriteString(focusMark(0) + "Токены (по одному на строку)\n")
|
||
b.WriteString(m.input.View())
|
||
b.WriteString("\n\n")
|
||
|
||
if m.errMsg != "" {
|
||
b.WriteString("Ошибка: " + m.errMsg + "\n\n")
|
||
}
|
||
|
||
b.WriteString(focusMark(1) + "Отчёт FIPS 140-2\n")
|
||
b.WriteString(m.output.View())
|
||
b.WriteString("\n\ntab — между полями · ctrl+r — запустить анализ\n")
|
||
b.WriteString("f1-f12, pgup — вкладки\n")
|
||
return b.String()
|
||
}
|