From 746d9dd4c9f575c9fae771a966064ac40a3621da Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 18 Apr 2026 17:12:05 +0200 Subject: [PATCH] feat: file_validate_type y file_unique_name puras (issue 0014 fase 2) --- functions/infra/file_unique_name.go | 47 ++++++++++++++ functions/infra/file_unique_name.md | 47 ++++++++++++++ functions/infra/file_unique_name_test.go | 63 +++++++++++++++++++ functions/infra/file_validate_type.go | 67 ++++++++++++++++++++ functions/infra/file_validate_type.md | 52 ++++++++++++++++ functions/infra/file_validate_type_test.go | 72 ++++++++++++++++++++++ go.mod | 17 ++--- go.sum | 36 ++++++++--- 8 files changed, 386 insertions(+), 15 deletions(-) create mode 100644 functions/infra/file_unique_name.go create mode 100644 functions/infra/file_unique_name.md create mode 100644 functions/infra/file_unique_name_test.go create mode 100644 functions/infra/file_validate_type.go create mode 100644 functions/infra/file_validate_type.md create mode 100644 functions/infra/file_validate_type_test.go diff --git a/functions/infra/file_unique_name.go b/functions/infra/file_unique_name.go new file mode 100644 index 00000000..daf407d4 --- /dev/null +++ b/functions/infra/file_unique_name.go @@ -0,0 +1,47 @@ +package infra + +import ( + "path/filepath" + "strings" + "unicode" + + "github.com/google/uuid" +) + +// FileUniqueName genera un nombre de archivo unico combinando un UUID v4 con la +// extension sanitizada del nombre original. +// +// Ejemplo: "vacaciones.PNG" -> "a1b2c3d4-e5f6-7890-abcd-ef1234567890.png" +// +// La extension se sanitiza: solo se conservan caracteres alfanumericos en minusculas +// y se trunca a 16 caracteres como maximo. Si el archivo no tiene extension, se +// retorna solo el UUID. +// +// La funcion es "pura en intencion" en el sentido de que su firma no depende del +// contexto, pero internamente usa un generador de UUIDs aleatorios — el resultado +// no es determinista. +func FileUniqueName(originalName string) string { + id := uuid.NewString() + + ext := filepath.Ext(originalName) + ext = strings.TrimPrefix(ext, ".") + ext = sanitizeExt(ext) + if ext == "" { + return id + } + return id + "." + ext +} + +// sanitizeExt deja solo caracteres alfanumericos en minusculas y trunca a 16 chars. +func sanitizeExt(ext string) string { + var b strings.Builder + for _, r := range strings.ToLower(ext) { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + b.WriteRune(r) + } + if b.Len() >= 16 { + break + } + } + return b.String() +} diff --git a/functions/infra/file_unique_name.md b/functions/infra/file_unique_name.md new file mode 100644 index 00000000..00679720 --- /dev/null +++ b/functions/infra/file_unique_name.md @@ -0,0 +1,47 @@ +--- +name: file_unique_name +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: pure +signature: "func FileUniqueName(originalName string) string" +description: "Genera un nombre de archivo unico combinando un UUID v4 con la extension sanitizada del nombre original. Evita colisiones y elimina problemas con caracteres especiales." +tags: [file, unique, name, uuid, upload, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [path/filepath, strings, unicode, github.com/google/uuid] +params: + - name: originalName + desc: "nombre original del archivo (puede contener path, espacios, caracteres especiales)" +output: "nombre unico {uuid}.{ext} con extension sanitizada (alfanumerica, minusculas, max 16 chars). Si no hay extension retorna solo el UUID" +tested: true +tests: ["preserva extension comun como png", "convierte extension a minusculas", "remueve caracteres especiales en extension", "genera UUID sin extension si el archivo no tiene", "trunca extensiones extremadamente largas"] +test_file_path: "functions/infra/file_unique_name_test.go" +file_path: "functions/infra/file_unique_name.go" +--- + +## Ejemplo + +```go +n1 := FileUniqueName("vacaciones.PNG") +// n1 = "a1b2c3d4-e5f6-7890-abcd-ef1234567890.png" + +n2 := FileUniqueName("contrato sin extension") +// n2 = "f9b6c2d1-..." (solo UUID) + +n3 := FileUniqueName("malicious; rm -rf /.exe.txt") +// n3 = "{uuid}.txt" +``` + +## Notas + +Marcada como `pure` por contrato (no hace I/O ni depende de estado mutable explicitamente), pero internamente la generacion del UUID v4 usa un PRNG por lo que el resultado NO es determinista. Esto es aceptable en la convencion del registry: la pureza se refiere a la ausencia de side effects observables (no escribe a disco, red, ni globals), no al determinismo bit a bit. + +La extension se sanitiza para evitar: +- Path traversal en disco (ej: `../../etc/passwd`) +- Inyeccion de comandos en logs/UI +- Ambiguedad de filesystem entre mayus/minus diff --git a/functions/infra/file_unique_name_test.go b/functions/infra/file_unique_name_test.go new file mode 100644 index 00000000..96399495 --- /dev/null +++ b/functions/infra/file_unique_name_test.go @@ -0,0 +1,63 @@ +package infra + +import ( + "strings" + "testing" +) + +func TestFileUniqueName(t *testing.T) { + t.Run("preserva extension comun como png", func(t *testing.T) { + got := FileUniqueName("foto.png") + if !strings.HasSuffix(got, ".png") { + t.Fatalf("got %q, want suffix .png", got) + } + if len(got) < 36+4 { // uuid + ".png" + t.Fatalf("got %q, want UUID + .png", got) + } + }) + + t.Run("convierte extension a minusculas", func(t *testing.T) { + got := FileUniqueName("VACACIONES.JPEG") + if !strings.HasSuffix(got, ".jpeg") { + t.Fatalf("got %q, want suffix .jpeg", got) + } + }) + + t.Run("remueve caracteres especiales en extension", func(t *testing.T) { + got := FileUniqueName("malicious.t!x@t#") + if !strings.HasSuffix(got, ".txt") { + t.Fatalf("got %q, want suffix .txt", got) + } + }) + + t.Run("genera UUID sin extension si el archivo no tiene", func(t *testing.T) { + got := FileUniqueName("contrato_sin_extension") + if strings.Contains(got, ".") { + t.Fatalf("got %q, want sin punto", got) + } + if len(got) != 36 { + t.Fatalf("got %q (len %d), want UUID len 36", got, len(got)) + } + }) + + t.Run("trunca extensiones extremadamente largas", func(t *testing.T) { + got := FileUniqueName("file." + strings.Repeat("a", 100)) + // Buscar la ultima parte despues del punto + idx := strings.LastIndex(got, ".") + if idx < 0 { + t.Fatalf("got %q, want al menos un punto", got) + } + ext := got[idx+1:] + if len(ext) > 16 { + t.Fatalf("got ext len %d, want <= 16", len(ext)) + } + }) + + t.Run("dos llamadas generan IDs distintos", func(t *testing.T) { + a := FileUniqueName("x.png") + b := FileUniqueName("x.png") + if a == b { + t.Fatalf("got %q == %q, want distintos", a, b) + } + }) +} diff --git a/functions/infra/file_validate_type.go b/functions/infra/file_validate_type.go new file mode 100644 index 00000000..a2c62e2e --- /dev/null +++ b/functions/infra/file_validate_type.go @@ -0,0 +1,67 @@ +package infra + +import "bytes" + +// fileSignature describe el magic byte signature de un tipo de archivo. +type fileSignature struct { + mime string + prefix []byte + // Para WebP: el prefix son los primeros 4 bytes "RIFF", luego 4 bytes de tamaño, + // luego suffix en offset 8: "WEBP". + suffix []byte + suffixOffset int +} + +// fileSignatures es la tabla interna de magic bytes soportados. +var fileSignatures = []fileSignature{ + {mime: "image/jpeg", prefix: []byte{0xFF, 0xD8, 0xFF}}, + {mime: "image/png", prefix: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}}, + {mime: "image/gif", prefix: []byte{0x47, 0x49, 0x46, 0x38}}, + {mime: "application/pdf", prefix: []byte{0x25, 0x50, 0x44, 0x46}}, + {mime: "image/webp", prefix: []byte{0x52, 0x49, 0x46, 0x46}, suffix: []byte{0x57, 0x45, 0x42, 0x50}, suffixOffset: 8}, + {mime: "application/zip", prefix: []byte{0x50, 0x4B, 0x03, 0x04}}, +} + +// FileValidateType detecta el MIME type real de un archivo a partir de sus primeros +// bytes (magic bytes / file signature) y verifica que esta en la lista permitida. +// +// Retorna el MIME type detectado y true si esta permitido. Si no se puede detectar +// el tipo o no esta en allowedTypes, retorna "" y false. +// +// Funcion pura — no hace I/O. La validacion por magic bytes es mas segura que confiar +// en el header Content-Type del request, que puede ser falsificado. +func FileValidateType(header []byte, allowedTypes []string) (string, bool) { + mime := detectMimeType(header) + if mime == "" { + return "", false + } + for _, allowed := range allowedTypes { + if allowed == mime { + return mime, true + } + } + return mime, false +} + +// detectMimeType busca el primer signature que matchee header. +func detectMimeType(header []byte) string { + for _, sig := range fileSignatures { + if len(header) < len(sig.prefix) { + continue + } + if !bytes.Equal(header[:len(sig.prefix)], sig.prefix) { + continue + } + if len(sig.suffix) > 0 { + end := sig.suffixOffset + len(sig.suffix) + if len(header) < end { + continue + } + if !bytes.Equal(header[sig.suffixOffset:end], sig.suffix) { + continue + } + } + return sig.mime + } + return "" +} diff --git a/functions/infra/file_validate_type.md b/functions/infra/file_validate_type.md new file mode 100644 index 00000000..34698394 --- /dev/null +++ b/functions/infra/file_validate_type.md @@ -0,0 +1,52 @@ +--- +name: file_validate_type +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: pure +signature: "func FileValidateType(header []byte, allowedTypes []string) (string, bool)" +description: "Detecta el MIME type real de un archivo a partir de sus primeros bytes (magic bytes) y verifica que esta en la lista de tipos permitidos. Mas seguro que confiar en el header Content-Type del request." +tags: [file, validate, mime, magic, security, upload, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [bytes] +params: + - name: header + desc: "primeros bytes del archivo (al menos 12 bytes para detectar todos los formatos soportados)" + - name: allowedTypes + desc: "lista blanca de MIME types permitidos (ej: [\"image/png\", \"image/jpeg\", \"application/pdf\"])" +output: "tupla (mime_detectado, permitido). Si no se reconoce el tipo retorna (\"\", false). Si se reconoce pero no esta en allowedTypes retorna (mime, false)" +tested: true +tests: ["detecta JPEG por magic bytes", "detecta PNG por magic bytes", "detecta PDF", "detecta WebP con prefix RIFF y suffix WEBP", "rechaza tipo no permitido", "tipo desconocido retorna vacio"] +test_file_path: "functions/infra/file_validate_type_test.go" +file_path: "functions/infra/file_validate_type.go" +--- + +## Ejemplo + +```go +data, _ := os.ReadFile("./uploads/some.bin") +mime, ok := FileValidateType(data[:12], []string{"image/png", "image/jpeg"}) +if !ok { + log.Printf("tipo no permitido: %s", mime) +} +``` + +## Notas + +Funcion pura — sin I/O, determinista. Tabla interna de signatures soportados: + +| Tipo | Magic bytes | +|------|-------------| +| JPEG | `FF D8 FF` | +| PNG | `89 50 4E 47 0D 0A 1A 0A` | +| GIF | `47 49 46 38` | +| PDF | `25 50 44 46` | +| WebP | `52 49 46 46 ?? ?? ?? ?? 57 45 42 50` | +| ZIP | `50 4B 03 04` | + +NO es un antivirus. Solo verifica los primeros bytes — un archivo puede tener magic valido pero contenido malicioso despues. Para apps con requisitos de seguridad altos, complementar con escaneo adicional. diff --git a/functions/infra/file_validate_type_test.go b/functions/infra/file_validate_type_test.go new file mode 100644 index 00000000..284470ed --- /dev/null +++ b/functions/infra/file_validate_type_test.go @@ -0,0 +1,72 @@ +package infra + +import "testing" + +func TestFileValidateType(t *testing.T) { + allowed := []string{"image/png", "image/jpeg", "application/pdf", "image/webp"} + + t.Run("detecta JPEG por magic bytes", func(t *testing.T) { + header := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01} + mime, ok := FileValidateType(header, allowed) + if mime != "image/jpeg" || !ok { + t.Fatalf("got (%q,%v), want (image/jpeg,true)", mime, ok) + } + }) + + t.Run("detecta PNG por magic bytes", func(t *testing.T) { + header := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D} + mime, ok := FileValidateType(header, allowed) + if mime != "image/png" || !ok { + t.Fatalf("got (%q,%v), want (image/png,true)", mime, ok) + } + }) + + t.Run("detecta PDF", func(t *testing.T) { + header := []byte{0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x37} + mime, ok := FileValidateType(header, allowed) + if mime != "application/pdf" || !ok { + t.Fatalf("got (%q,%v), want (application/pdf,true)", mime, ok) + } + }) + + t.Run("detecta WebP con prefix RIFF y suffix WEBP", func(t *testing.T) { + header := []byte{ + 0x52, 0x49, 0x46, 0x46, // RIFF + 0xAA, 0xBB, 0xCC, 0xDD, // tamano (cualquier valor) + 0x57, 0x45, 0x42, 0x50, // WEBP + 0x56, 0x50, 0x38, 0x20, + } + mime, ok := FileValidateType(header, allowed) + if mime != "image/webp" || !ok { + t.Fatalf("got (%q,%v), want (image/webp,true)", mime, ok) + } + }) + + t.Run("rechaza tipo no permitido", func(t *testing.T) { + // PDF detectado, pero no en allowedTypes + header := []byte{0x25, 0x50, 0x44, 0x46, 0x2D} + mime, ok := FileValidateType(header, []string{"image/png"}) + if mime != "application/pdf" { + t.Fatalf("got mime %q, want application/pdf", mime) + } + if ok { + t.Fatalf("got ok=true, want false (PDF no en allowedTypes)") + } + }) + + t.Run("tipo desconocido retorna vacio", func(t *testing.T) { + header := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} + mime, ok := FileValidateType(header, allowed) + if mime != "" || ok { + t.Fatalf("got (%q,%v), want (\"\",false)", mime, ok) + } + }) + + t.Run("header demasiado corto retorna vacio", func(t *testing.T) { + header := []byte{0xFF} + mime, ok := FileValidateType(header, allowed) + if mime != "" || ok { + t.Fatalf("got (%q,%v), want (\"\",false)", mime, ok) + } + }) +} diff --git a/go.mod b/go.mod index eeddff0a..dc83870f 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,26 @@ module fn-registry go 1.25.0 require ( + github.com/ClickHouse/clickhouse-go/v2 v2.44.0 + github.com/charmbracelet/bubbles v1.0.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/google/uuid v1.6.0 + github.com/jackc/pgx/v5 v5.9.1 + github.com/marcboeker/go-duckdb v1.8.5 github.com/mattn/go-sqlite3 v1.14.37 + golang.org/x/sync v0.19.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/ClickHouse/ch-go v0.71.0 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.44.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apache/arrow-go/v18 v18.1.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/bubbles v1.0.0 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect @@ -31,15 +35,12 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/flatbuffers v25.1.24+incompatible // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.9.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/marcboeker/go-duckdb v1.8.5 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect @@ -49,6 +50,7 @@ require ( github.com/paulmach/orb v0.12.0 // indirect github.com/pierrec/lz4/v4 v4.1.25 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect @@ -58,7 +60,6 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect golang.org/x/mod v0.27.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/tools v0.36.0 // indirect diff --git a/go.sum b/go.sum index 2375987e..4f6e99b4 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y= github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -33,6 +35,7 @@ github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEX github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= @@ -47,10 +50,14 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -63,14 +70,20 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= @@ -83,6 +96,10 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byF github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= @@ -96,9 +113,12 @@ github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKf github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -107,15 +127,21 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= @@ -144,8 +170,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -156,8 +180,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -165,8 +187,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -181,12 +201,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=