feat(cybersecurity): auto-commit con 48 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ansiCSI matches CSI sequences: ESC [ ... <final byte>
|
||||
// Covers colors (SGR), cursor movement, erase, etc.
|
||||
var ansiCSI = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
|
||||
|
||||
// ansiOSC matches OSC sequences: ESC ] ... <BEL or ST>
|
||||
// Used for window titles, hyperlinks, etc.
|
||||
var ansiOSC = regexp.MustCompile(`\x1b\][^\x07\x1b]*(\x07|\x1b\\)`)
|
||||
|
||||
// ansiEsc matches other two-character escape sequences: ESC <char>
|
||||
// Covers ESC c (reset), ESC ( B, ESC ) 0, etc.
|
||||
var ansiEsc = regexp.MustCompile(`\x1b[@-Z\\-_]|\x1b[()][0-9A-Za-z]`)
|
||||
|
||||
// StripANSI removes ANSI/VT100 terminal escape sequences from s and filters
|
||||
// non-printable control characters, preserving newlines (\n), tabs (\t) and
|
||||
// carriage returns (\r).
|
||||
func StripANSI(s string) string {
|
||||
s = ansiCSI.ReplaceAllString(s, "")
|
||||
s = ansiOSC.ReplaceAllString(s, "")
|
||||
s = ansiEsc.ReplaceAllString(s, "")
|
||||
return strings.Map(func(r rune) rune {
|
||||
// Preserve printable characters, \n (0x0A), \t (0x09), \r (0x0D).
|
||||
if r == '\n' || r == '\t' || r == '\r' {
|
||||
return r
|
||||
}
|
||||
// Drop C0 control characters (0x00-0x1F) and DEL (0x7F).
|
||||
if r < 0x20 || r == 0x7F {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: strip_ansi
|
||||
kind: function
|
||||
lang: go
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func StripANSI(s string) string"
|
||||
description: "Elimina secuencias de escape ANSI/VT100 de un string y filtra caracteres de control no imprimibles, preservando \\n, \\t y \\r."
|
||||
tags: ["terminal", "ansi", "string", "sanitize", "terminal-capture"]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: ["regexp", "strings"]
|
||||
params:
|
||||
- name: s
|
||||
desc: "String que puede contener secuencias de escape de terminal (CSI, OSC, escapes simples) y/o caracteres de control."
|
||||
output: "String limpio: sin secuencias ANSI ni caracteres de control, preservando saltos de línea (\\n), tabulaciones (\\t) y retornos de carro (\\r)."
|
||||
tested: true
|
||||
tests:
|
||||
- "golden: color SGR codes"
|
||||
- "edge OSC titulo de ventana"
|
||||
- "edge movimientos de cursor"
|
||||
- "edge string sin escapes preserva saltos de linea"
|
||||
- "edge string vacio"
|
||||
- "edge preserva tabs"
|
||||
test_file_path: "functions/core/strip_ansi_test.go"
|
||||
file_path: "functions/core/strip_ansi.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Limpiar output de terminal con color rojo
|
||||
raw := "\x1b[31mError:\x1b[0m archivo no encontrado"
|
||||
clean := core.StripANSI(raw)
|
||||
// clean == "Error: archivo no encontrado"
|
||||
|
||||
// Limpiar título de ventana OSC
|
||||
raw2 := "\x1b]0;mi titulo\x07contenido real"
|
||||
clean2 := core.StripANSI(raw2)
|
||||
// clean2 == "contenido real"
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando captures output de un PTY/TUI/subprocess y necesites texto plano: antes de indexar logs con ANSI en un buscador, antes de difar output de terminal, o cuando muestres salida de comando en un contexto sin soporte de escape (UI web, archivo, base de datos).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Preserva `\n`, `\t` y `\r` a propósito: el output de terminales suele tener CRLF y tabulaciones con semántica propia.
|
||||
- Cubre CSI, OSC y escapes simples de dos caracteres. Secuencias DCS o PM (rarísimas) no se eliminan; si las necesitas, añade una regex adicional antes de llamar a esta función.
|
||||
- Las regexes están precompiladas a nivel de paquete: no hay coste de compilación por llamada.
|
||||
@@ -0,0 +1,53 @@
|
||||
package core
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStripANSI(t *testing.T) {
|
||||
t.Run("golden: color SGR codes", func(t *testing.T) {
|
||||
got := StripANSI("\x1b[31mhola\x1b[0m mundo")
|
||||
want := "hola mundo"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge OSC titulo de ventana", func(t *testing.T) {
|
||||
got := StripANSI("\x1b]0;mi titulo\x07texto")
|
||||
want := "texto"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge movimientos de cursor", func(t *testing.T) {
|
||||
got := StripANSI("linea1\x1b[2K\x1b[1Glinea2")
|
||||
want := "linea1linea2"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge string sin escapes preserva saltos de linea", func(t *testing.T) {
|
||||
got := StripANSI("plano\ncon\nlineas")
|
||||
want := "plano\ncon\nlineas"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge string vacio", func(t *testing.T) {
|
||||
got := StripANSI("")
|
||||
want := ""
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge preserva tabs", func(t *testing.T) {
|
||||
got := StripANSI("a\tb")
|
||||
want := "a\tb"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package core
|
||||
|
||||
// PrefixDelta returns the portion of curr that follows the longest common
|
||||
// prefix (LCP) shared with prev, comparing rune-by-rune to avoid splitting
|
||||
// multi-byte characters.
|
||||
//
|
||||
// In the monotone streaming case (curr = prev + new), this returns exactly
|
||||
// the new suffix. When the text diverges mid-way (reflow), it returns
|
||||
// everything from the point of divergence to the end of curr.
|
||||
func PrefixDelta(prev, curr string) string {
|
||||
prevRunes := []rune(prev)
|
||||
currRunes := []rune(curr)
|
||||
|
||||
common := 0
|
||||
for common < len(prevRunes) && common < len(currRunes) {
|
||||
if prevRunes[common] != currRunes[common] {
|
||||
break
|
||||
}
|
||||
common++
|
||||
}
|
||||
|
||||
return string(currRunes[common:])
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: text_prefix_delta
|
||||
kind: function
|
||||
lang: go
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func PrefixDelta(prev, curr string) string"
|
||||
description: "Calcula el delta de streaming entre dos versiones de un texto: devuelve la porción de curr que sigue al prefijo común más largo con prev, comparando runa a runa para no partir caracteres multibyte."
|
||||
tags: [string, diff, streaming, delta, terminal-capture]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: prev
|
||||
desc: "Versión anterior del texto acumulativo (snapshot anterior del stream)."
|
||||
- name: curr
|
||||
desc: "Versión actual del texto acumulativo (snapshot actual, normalmente extiende a prev)."
|
||||
output: "La porción de curr que sigue al prefijo común con prev (el 'delta' de streaming). Devuelve cadena vacía si curr no añade nada nuevo tras el prefijo común."
|
||||
tested: true
|
||||
tests:
|
||||
- "monotono append normal"
|
||||
- "prev vacio devuelve curr completo"
|
||||
- "sin cambios devuelve vacio"
|
||||
- "divergencia en medio devuelve desde divergencia"
|
||||
- "curr mas corto que prev devuelve vacio"
|
||||
- "multibyte cafe streaming"
|
||||
- "multibyte prefijo parcial antes de acento"
|
||||
- "ambos vacios devuelve vacio"
|
||||
- "prev no vacio curr vacio devuelve vacio"
|
||||
- "determinismo misma entrada misma salida"
|
||||
test_file_path: "functions/core/text_prefix_delta_test.go"
|
||||
file_path: "functions/core/text_prefix_delta.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Bucle de streaming por snapshots acumulativos:
|
||||
prev := ""
|
||||
snapshots := []string{"Hola", "Hola, mun", "Hola, mundo!"}
|
||||
|
||||
for _, curr := range snapshots {
|
||||
delta := PrefixDelta(prev, curr)
|
||||
if delta != "" {
|
||||
fmt.Print(delta) // emite solo la parte nueva
|
||||
}
|
||||
prev = curr
|
||||
}
|
||||
// Output: Hola, mundo!
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando hagas streaming por snapshots acumulativos y necesites emitir solo la parte nueva de cada snapshot. Caso típico: consumir `pty_capture_stream_go_infra` donde cada captura de la TUI es un snapshot que extiende al anterior, y quieres emitir eventos `text_delta` estilo SSE/streaming sin reenviar texto ya enviado.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Compara por prefijo común, no por diff completo. Si el texto cambia en medio (reflow, borrado, sobreescritura de terminal), el delta incluye todo desde el punto de divergencia hasta el final de curr — puede re-emitir texto ya visto. Adecuado para append monótono; en streaming de TUI con reflow es heurístico, no exacto.
|
||||
- Trabaja sobre runas (no bytes) para no partir caracteres UTF-8 multibyte como 'é', '中', '→'. El offset de corte siempre cae en un límite de runa válido.
|
||||
@@ -0,0 +1,87 @@
|
||||
package core
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPrefixDelta(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prev string
|
||||
curr string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "monotono append normal",
|
||||
prev: "PON",
|
||||
curr: "PONG",
|
||||
want: "G",
|
||||
},
|
||||
{
|
||||
name: "prev vacio devuelve curr completo",
|
||||
prev: "",
|
||||
curr: "abc",
|
||||
want: "abc",
|
||||
},
|
||||
{
|
||||
name: "sin cambios devuelve vacio",
|
||||
prev: "abc",
|
||||
curr: "abc",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "divergencia en medio devuelve desde divergencia",
|
||||
prev: "abc",
|
||||
curr: "abXY",
|
||||
want: "XY",
|
||||
},
|
||||
{
|
||||
name: "curr mas corto que prev devuelve vacio",
|
||||
prev: "abcdef",
|
||||
curr: "abc",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "multibyte cafe streaming",
|
||||
prev: "café",
|
||||
curr: "café con leche",
|
||||
want: " con leche",
|
||||
},
|
||||
{
|
||||
name: "multibyte prefijo parcial antes de acento",
|
||||
prev: "ca",
|
||||
curr: "café",
|
||||
want: "fé",
|
||||
},
|
||||
{
|
||||
name: "ambos vacios devuelve vacio",
|
||||
prev: "",
|
||||
curr: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "prev no vacio curr vacio devuelve vacio",
|
||||
prev: "hola",
|
||||
curr: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "determinismo misma entrada misma salida",
|
||||
prev: "hello world",
|
||||
curr: "hello world!",
|
||||
want: "!",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := PrefixDelta(tc.prev, tc.curr)
|
||||
if got != tc.want {
|
||||
t.Errorf("PrefixDelta(%q, %q) = %q, want %q", tc.prev, tc.curr, got, tc.want)
|
||||
}
|
||||
// Verificar determinismo: segunda llamada produce el mismo resultado.
|
||||
got2 := PrefixDelta(tc.prev, tc.curr)
|
||||
if got != got2 {
|
||||
t.Errorf("no determinista: primera=%q segunda=%q", got, got2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user