mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-13 10:04:18 +08:00
- SQL、CSV、JSON 与 XLSX 改为资源受限的流式解析,并支持压缩、编码和方言预检 - 增加遇错停止/继续策略、持久任务、错误行导出、源文件身份与安全取消 - 为数据库驱动补齐上下文事务和写入结果未知语义,避免失败重放与脏会话复用
241 lines
7.3 KiB
Go
241 lines
7.3 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
func TestOpenSQLImportSourceStripsUTF8BOM(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql")
|
|
if err := os.WriteFile(path, append([]byte{0xef, 0xbb, 0xbf}, []byte("SELECT 1;")...), 0o600); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
got, err := io.ReadAll(source)
|
|
if err != nil {
|
|
t.Fatalf("read SQL import source: %v", err)
|
|
}
|
|
if string(got) != "SELECT 1;" {
|
|
t.Fatalf("decoded source = %q, want BOM-free UTF-8", got)
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceDecodesUTF16LE(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql")
|
|
if err := os.WriteFile(path, encodeUTF16SQL("SELECT '中文';", binary.LittleEndian), 0o600); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
got, err := io.ReadAll(source)
|
|
if err != nil {
|
|
t.Fatalf("read SQL import source: %v", err)
|
|
}
|
|
if string(got) != "SELECT '中文';" || source.Encoding != "utf-16le" {
|
|
t.Fatalf("decoded source = %q (%s), want UTF-16LE decoded as UTF-8", got, source.Encoding)
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceDecodesUTF16BE(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql")
|
|
if err := os.WriteFile(path, encodeUTF16SQL("SELECT '中文';", binary.BigEndian), 0o600); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
got, err := io.ReadAll(source)
|
|
if err != nil {
|
|
t.Fatalf("read SQL import source: %v", err)
|
|
}
|
|
if string(got) != "SELECT '中文';" || source.Encoding != "utf-16be" {
|
|
t.Fatalf("decoded source = %q (%s), want UTF-16BE decoded as UTF-8", got, source.Encoding)
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceStreamsGzipBeforeEncodingDetection(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql.gz")
|
|
writeGzipSQL(t, path, append([]byte{0xef, 0xbb, 0xbf}, []byte("SELECT 1;")...))
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
got, err := io.ReadAll(source)
|
|
if err != nil {
|
|
t.Fatalf("read SQL import source: %v", err)
|
|
}
|
|
if string(got) != "SELECT 1;" || !source.Compressed {
|
|
t.Fatalf("decoded source = %q (compressed=%v), want streamed gzip SQL", got, source.Compressed)
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceReportsAndObservesRawCompressedBytes(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql.gz")
|
|
writeGzipSQL(t, path, []byte("SELECT 1;"))
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read raw source: %v", err)
|
|
}
|
|
var observed bytes.Buffer
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{RawObserver: &observed})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
if _, err := io.ReadAll(source); err != nil {
|
|
t.Fatalf("read SQL import source: %v", err)
|
|
}
|
|
if source.RawBytesRead() != int64(len(raw)) {
|
|
t.Fatalf("raw bytes read = %d, want compressed size %d", source.RawBytesRead(), len(raw))
|
|
}
|
|
if !bytes.Equal(observed.Bytes(), raw) {
|
|
t.Fatal("raw observer did not receive the original compressed bytes")
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceEnforcesDecodedByteLimit(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql")
|
|
if err := os.WriteFile(path, []byte("SELECT 1234567890;"), 0o600); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{MaxDecodedBytes: 8})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
got, readErr := io.ReadAll(source)
|
|
var limitErr *SQLImportSourceLimitError
|
|
if !errors.As(readErr, &limitErr) || limitErr.Kind != SQLImportSourceDecodedByteLimit {
|
|
t.Fatalf("read error = %v, want decoded-byte limit error", readErr)
|
|
}
|
|
if len(got) > 8 || limitErr.DecodedBytes != 8 || limitErr.Limit != 8 {
|
|
t.Fatalf("read %d bytes, error = %#v; want no bytes beyond limit", len(got), limitErr)
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceRejectsExcessiveCompressionRatio(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql.gz")
|
|
writeGzipSQL(t, path, []byte(strings.Repeat("INSERT INTO t VALUES (1);\n", 4096)))
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{
|
|
MaxDecodedBytes: 1 << 20,
|
|
MaxCompressionRatio: 2,
|
|
MinCompressedBytesForRatio: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
_, readErr := io.ReadAll(source)
|
|
var limitErr *SQLImportSourceLimitError
|
|
if !errors.As(readErr, &limitErr) || limitErr.Kind != SQLImportSourceCompressionRatio {
|
|
t.Fatalf("read error = %v, want compression-ratio limit error", readErr)
|
|
}
|
|
if limitErr.Ratio <= 2 || limitErr.MaxCompressionRatio != 2 || limitErr.CompressedBytes <= 0 {
|
|
t.Fatalf("limit error = %#v, want measured ratio above limit", limitErr)
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceDefaultCompressionRatioProtectsSmallGzipBomb(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql.gz")
|
|
writeGzipSQL(t, path, []byte(strings.Repeat("A", 8<<20)))
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
_, readErr := io.ReadAll(source)
|
|
var limitErr *SQLImportSourceLimitError
|
|
if !errors.As(readErr, &limitErr) || limitErr.Kind != SQLImportSourceCompressionRatio {
|
|
t.Fatalf("read error = %v, want default compression-ratio protection", readErr)
|
|
}
|
|
}
|
|
|
|
func TestOpenSQLImportSourceReportsDecodedLimitBeforeRatioWhenBothCross(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "dump.sql.gz")
|
|
writeGzipSQL(t, path, []byte(strings.Repeat("A", 4096)))
|
|
|
|
source, err := OpenSQLImportSource(path, SQLImportSourceOptions{
|
|
MaxDecodedBytes: 8,
|
|
MaxCompressionRatio: 0.01,
|
|
MinCompressedBytesForRatio: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("open SQL import source: %v", err)
|
|
}
|
|
defer source.Close()
|
|
|
|
_, readErr := io.ReadAll(source)
|
|
var limitErr *SQLImportSourceLimitError
|
|
if !errors.As(readErr, &limitErr) || limitErr.Kind != SQLImportSourceDecodedByteLimit {
|
|
t.Fatalf("read error = %v, want earlier decoded-byte limit", readErr)
|
|
}
|
|
}
|
|
|
|
func writeGzipSQL(t *testing.T, path string, payload []byte) {
|
|
t.Helper()
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("create gzip source: %v", err)
|
|
}
|
|
writer := gzip.NewWriter(file)
|
|
if _, err := writer.Write(payload); err != nil {
|
|
_ = file.Close()
|
|
t.Fatalf("write gzip source: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
_ = file.Close()
|
|
t.Fatalf("close gzip writer: %v", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
t.Fatalf("close gzip source: %v", err)
|
|
}
|
|
}
|
|
|
|
func encodeUTF16SQL(value string, order binary.ByteOrder) []byte {
|
|
encoded := utf16.Encode([]rune(value))
|
|
result := make([]byte, 2, 2+len(encoded)*2)
|
|
if order == binary.LittleEndian {
|
|
result[0], result[1] = 0xff, 0xfe
|
|
} else {
|
|
result[0], result[1] = 0xfe, 0xff
|
|
}
|
|
for _, codeUnit := range encoded {
|
|
result = append(result, 0, 0)
|
|
order.PutUint16(result[len(result)-2:], codeUnit)
|
|
}
|
|
return result
|
|
}
|