From 181c1f0be3cce0554c38be0a45a6d71b19b3a081 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Fri, 15 May 2026 01:55:34 +0800 Subject: [PATCH] feat: initial commit of docDB (extracted from knowbase) (by AI) --- .gitignore | 7 ++ CHANGELOG.md | 11 ++ README.md | 72 ++++++++++++ TEST.md | 10 ++ doc.go | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++ doc_test.go | 130 ++++++++++++++++++++++ go.mod | 67 +++++++++++ go.sum | 72 ++++++++++++ 8 files changed, 678 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 TEST.md create mode 100644 doc.go create mode 100644 doc_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5c7525 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.geminiignore +.gemini +.ai/ +env.json +env.yml +env.yaml +.log.meta.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3126fc8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# CHANGELOG + +## [1.0.0] - 2026-05-15 +### Added +- 从 `knowbase` 项目剥离生成的独立 `docDB` 项目。 +- 支持基于 `tableDB` 的文档存储,自动继承权限隔离与数据备份能力。 +- 强制区分 `SetDoc` (版本提升) 与 `SetMeta` (元数据更新)。 +- 完整的生命周期事件 Hooks (`OnCreatedDoc`, `OnUpdatedDoc`, `OnRemoved`)。 +- 支持流式大文件上传与落盘。 +- 自动提取 Markdown 目录结构 (ToC)。 +- 迁移并验证了所有核心测试用例。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..575ba82 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# @go/docDB + +`docDB` 是一个独立的高级文档存储引擎,基于 `@go/tableDB` 构建,提供版本管理、流式上传及生命周期钩子。 + +## 📦 安装 + +```bash +go get apigo.cc/go/docDB +``` + +## 🛠 核心功能 + +1. **权限隔离**:完全继承 `tableDB` 的用户隔离体系。 +2. **版本管控**:通过 `SetDoc` 强制提升文档版本,`SetMeta` 仅更新属性而不影响版本。 +3. **流式上传**:支持 `io.Reader` 接口,低内存占用处理大文件落盘。 +4. **事件驱动**:提供 `OnCreatedDoc`, `OnUpdatedDoc`, `OnRemoved` 钩子,方便集成搜索索引、异步处理等。 +5. **结构化目录**:自动提取 Markdown 文档的目录树 (ToC)。 + +## 🚀 快速开始 + +### 初始化 + +```go +import ( + "apigo.cc/go/docDB" + "apigo.cc/go/tableDB" +) + +// 创建 DocDB 实例 +db := tableDB.GetDB("sqlite://docs.db", logger) +docStore := docDB.New(db, "./storage") + +// 授权并获取 App +app := docStore.Auth("user_123") +``` + +### 存储文档 + +```go +doc := &docDB.Document{ + Path: "/projects/readme.md", + Title: "Getting Started", + TextContent: "# Hello World\n...", + BinaryContent: fileReader, // 可选,流式上传 +} + +// 保存文档 (版本提升) +err := app.SetDoc(doc) +``` + +### 更新元数据 + +```go +// 仅更新标签,不提升版本 +err := app.SetMeta(docID, map[string]any{ + "Tags": "tutorial,go", +}) +``` + +### 监听事件 + +```go +docStore.OnCreatedDoc(func(doc *docDB.Document) { + fmt.Println("New doc created:", doc.ID) + // 可以在这里触发异步摘要生成或向量化 +}) +``` + +## 📝 注意事项 + +- **数据对齐**:内部强制将 `Document.ID` 对齐为 `tableDB` 约定的 `id` 字段。 +- **无状态性**:不包含具体业务逻辑,如摘要生成、全文搜索等,推荐通过事件钩子实现。 diff --git a/TEST.md b/TEST.md new file mode 100644 index 0000000..a696af5 --- /dev/null +++ b/TEST.md @@ -0,0 +1,10 @@ +=== RUN TestDocDB +--- PASS: TestDocDB (0.12s) +goos: darwin +goarch: amd64 +pkg: apigo.cc/go/docDB +cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz +BenchmarkExtractToC +BenchmarkExtractToC-16 461220 2657 ns/op +PASS +ok apigo.cc/go/docDB 2.586s diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..8ae45f9 --- /dev/null +++ b/doc.go @@ -0,0 +1,309 @@ +package docDB + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "apigo.cc/go/cast" + "apigo.cc/go/file" + "apigo.cc/go/id" + "apigo.cc/go/tableDB" +) + +// DocDB 实例 +type DocDB struct { + db *tableDB.TableDBUnauthorized + baseDir string + onCreatedDoc func(doc *Document) + onUpdatedDoc func(doc *Document) + onRemoved func(id string) +} + +// App 实例 (带用户权限) +type App struct { + docDB *DocDB + userID string + table *tableDB.Table +} + +// Document 结构体 +type Document struct { + ID string // 唯一标识符 + Path string // 层级路径 + Title string // 标题 + TextContent string // 纯文本 + BinaryContent io.Reader `json:"-"` // 仅为内存瞬态,流式读取 + FilePath string // 原文件落盘绝对路径 + Thumbnail string // 缩略图路径 + Summary string // 摘要 + Metadata map[string]any // 元数据 + ToC []ToCNode // 目录树 + Tags string // 标签 + Language string // 语言 + Type string // 类型 + StoreType string // 存储方式 (file/db) + AsFile bool // 是否以文件存储 + Version uint64 // 版本号 + IsSecret bool // 是否加密 + CreateTime int64 // 创建时间 + UpdateTime int64 // 更新时间 + Creator string // 创建人 ID + Updater string // 更新人 ID +} + +// ToCNode 目录树节点 +type ToCNode struct { + Title string + Level int + Children []ToCNode +} + +// New 创建 DocDB 实例 +func New(db *tableDB.TableDBUnauthorized, baseDir string) *DocDB { + return &DocDB{ + db: db, + baseDir: baseDir, + } +} + +// Auth 授权并初始化表结构 +func (d *DocDB) Auth(userID string) *App { + appTableDB := d.db.Auth(userID) + + // 检查 _Doc 表是否存在,不存在则初始化 + res, _ := appTableDB.Table("_Table").Get("_Doc") + if res == nil { + // 使用高级接口 SetTable 和 SetField + _ = appTableDB.SetTable(tableDB.TableSchema{ID: "_Doc", Name: "_Doc", Memo: "Document Repository"}) + _ = appTableDB.Table("_Doc").SetField( + tableDB.FieldSchema{Name: "path", Type: "v256", IsIndex: true}, + tableDB.FieldSchema{Name: "title", Type: "v255"}, + tableDB.FieldSchema{Name: "textContent", Type: "t"}, + tableDB.FieldSchema{Name: "filePath", Type: "v256"}, + tableDB.FieldSchema{Name: "thumbnail", Type: "v256"}, + tableDB.FieldSchema{Name: "summary", Type: "t"}, + tableDB.FieldSchema{Name: "metadata", Type: "o"}, + tableDB.FieldSchema{Name: "toc", Type: "o"}, + tableDB.FieldSchema{Name: "tags", Type: "v1024"}, + tableDB.FieldSchema{Name: "language", Type: "v32"}, + tableDB.FieldSchema{Name: "type", Type: "v32"}, + tableDB.FieldSchema{Name: "storeType", Type: "v16"}, + tableDB.FieldSchema{Name: "asFile", Type: "b"}, + tableDB.FieldSchema{Name: "version", Type: "bi"}, + tableDB.FieldSchema{Name: "isSecret", Type: "b"}, + tableDB.FieldSchema{Name: "updateTime", Type: "bi"}, + tableDB.FieldSchema{Name: "updater", Type: "v64"}, + tableDB.FieldSchema{Name: "createTime", Type: "bi"}, + tableDB.FieldSchema{Name: "creator", Type: "v64"}, + ) + } + + return &App{ + docDB: d, + userID: userID, + table: appTableDB.Table("_Doc"), + } +} + +// SetDoc 更新内容 (强制提升版本号) +func (a *App) SetDoc(docObj *Document) error { + if docObj.ID == "" { + docObj.ID = id.MakeID(10) + } + + now := time.Now().UnixMilli() + existing, err := a.table.Get(docObj.ID) + + isUpdate := err == nil && existing != nil + + if isUpdate { + docObj.Version = cast.Uint64(existing["version"]) + 1 + docObj.CreateTime = cast.Int64(existing["createTime"]) + docObj.Creator = cast.String(existing["creator"]) + } else { + docObj.Version = 1 + docObj.CreateTime = now + docObj.Creator = a.userID + } + + docObj.UpdateTime = now + docObj.Updater = a.userID + + // 处理文件落盘 + if docObj.BinaryContent != nil { + if err := a.saveFile(docObj); err != nil { + return err + } + docObj.AsFile = true + docObj.StoreType = "file" + } + + // 自动提取 ToC + if docObj.ToC == nil && (docObj.Type == "markdown" || strings.HasSuffix(strings.ToLower(docObj.Path), ".md")) { + docObj.ToC = ExtractToC(docObj.TextContent) + } + + // 转换为 map 并清理瞬态/冲突字段 + record := a.toRecord(docObj) + + err = a.table.Set(record) + if err == nil { + if isUpdate { + a.docDB.triggerUpdated(docObj) + } else { + a.docDB.triggerCreated(docObj) + } + } + return err +} + +// SetMeta 更新元数据 (不提升版本号) +func (a *App) SetMeta(id string, meta map[string]any) error { + existing, err := a.table.Get(id) + if err != nil || existing == nil { + return fmt.Errorf("document not found: %s", id) + } + + now := time.Now().UnixMilli() + for k, v := range meta { + existing[k] = v + } + existing["updateTime"] = now + existing["updater"] = a.userID + + return a.table.Set(existing) +} + +func (a *App) toRecord(docObj *Document) map[string]any { + record := make(map[string]any) + cast.Convert(&record, docObj) + + // tableDB 内部约定使用小写 id,这里强制转换以避免产生重复字段 + if v, ok := record["ID"]; ok { + record["id"] = v + delete(record, "ID") + } + + // 移除瞬态字段 + delete(record, "binaryContent") + + return record +} + +func (a *App) saveFile(docObj *Document) error { + subDir1 := docObj.ID[0:2] + subDir2 := docObj.ID[2:6] + subDir3 := docObj.ID[6:10] + + ext := filepath.Ext(docObj.Path) + fileName := fmt.Sprintf("v%d%s", docObj.Version, ext) + filePath := filepath.Join(a.docDB.baseDir, "doc", subDir1, subDir2, subDir3, fileName) + file.EnsureParentDir(filePath) + + out, err := os.Create(filePath) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, docObj.BinaryContent); err != nil { + return err + } + + docObj.FilePath = filePath + return nil +} + +// Get 获取文档 +func (a *App) Get(id string) (*Document, error) { + res, err := a.table.Get(id) + if err != nil || res == nil { + return nil, err + } + var doc Document + cast.Convert(&doc, res) + return &doc, nil +} + +// Remove 删除文档 +func (a *App) Remove(id string) error { + err := a.table.Remove(id) + if err == nil { + a.docDB.triggerRemoved(id) + } + return err +} + +// 事件订阅 +func (d *DocDB) OnCreatedDoc(fn func(doc *Document)) { d.onCreatedDoc = fn } +func (d *DocDB) OnUpdatedDoc(fn func(doc *Document)) { d.onUpdatedDoc = fn } +func (d *DocDB) OnRemoved(fn func(id string)) { d.onRemoved = fn } + +func (d *DocDB) triggerCreated(doc *Document) { + if d.onCreatedDoc != nil { + d.onCreatedDoc(doc) + } +} + +func (d *DocDB) triggerUpdated(doc *Document) { + if d.onUpdatedDoc != nil { + d.onUpdatedDoc(doc) + } +} + +func (d *DocDB) triggerRemoved(id string) { + if d.onRemoved != nil { + d.onRemoved(id) + } +} + +// ExtractToC 保持提取目录逻辑 +func ExtractToC(content string) []ToCNode { + lines := strings.Split(content, "\n") + var root []ToCNode + stack := []*[]ToCNode{&root} + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if !strings.HasPrefix(trimmed, "#") { + continue + } + + level := 0 + for level < len(trimmed) && trimmed[level] == '#' { + level++ + } + if level > 0 && level < len(trimmed) && trimmed[level] == ' ' { + title := strings.TrimSpace(trimmed[level:]) + + for len(stack) > level { + stack = stack[:len(stack)-1] + } + + for len(stack) < level { + parentSlice := stack[len(stack)-1] + if len(*parentSlice) == 0 { + *parentSlice = append(*parentSlice, ToCNode{ + Title: "...", + Level: len(stack), + }) + } + lastNode := &((*parentSlice)[len(*parentSlice)-1]) + stack = append(stack, &(lastNode.Children)) + } + + parentSlice := stack[len(stack)-1] + *parentSlice = append(*parentSlice, ToCNode{ + Title: title, + Level: level, + }) + lastNode := &((*parentSlice)[len(*parentSlice)-1]) + stack = append(stack, &(lastNode.Children)) + } + } + return root +} diff --git a/doc_test.go b/doc_test.go new file mode 100644 index 0000000..8d4f786 --- /dev/null +++ b/doc_test.go @@ -0,0 +1,130 @@ +package docDB + +import ( + "bytes" + "os" + "strings" + "testing" + + "apigo.cc/go/log" + "apigo.cc/go/tableDB" +) + +func TestDocDB(t *testing.T) { + logger := log.DefaultLogger + dbFile := "test_doc.db" + os.Remove(dbFile) + defer os.Remove(dbFile) + os.RemoveAll("./test_docs") + defer os.RemoveAll("./test_docs") + + unauthorizedDB := tableDB.GetDB("sqlite://"+dbFile, logger) + + systemApp := unauthorizedDB.Auth("_system") + // Bootstrap system tables via the special :Schema table + _ = systemApp.Table("_Table:Schema").Set(map[string]any{"dsl": ""}) + + docDBInst := New(unauthorizedDB, "./test_docs") + app := docDBInst.Auth("user1") + + var createdCalled, updatedCalled, removedCalled bool + docDBInst.OnCreatedDoc(func(doc *Document) { createdCalled = true }) + docDBInst.OnUpdatedDoc(func(doc *Document) { updatedCalled = true }) + docDBInst.OnRemoved(func(id string) { removedCalled = true }) + + // 1. 测试 SetDoc (创建) + doc1 := &Document{ + Path: "/test/doc1.md", + Title: "Test Doc", + TextContent: "# Header 1\nContent 1", + Type: "markdown", + } + + err := app.SetDoc(doc1) + if err != nil { + t.Fatalf("SetDoc failed: %v", err) + } + + if doc1.Version != 1 { + t.Fatalf("Expected version 1, got %d", doc1.Version) + } + if !createdCalled { + t.Error("OnCreatedDoc event not triggered") + } + + // 2. 测试 Get + d, err := app.Get(doc1.ID) + if err != nil || d == nil { + t.Fatalf("Get failed: %v", err) + } + if d.Title != "Test Doc" { + t.Fatalf("Expected title 'Test Doc', got %s", d.Title) + } + if len(d.ToC) == 0 { + t.Error("ToC should be extracted") + } + + // 3. 测试 SetDoc (更新内容, 提升版本) + doc1.TextContent = "# Header 1\nUpdated Content" + err = app.SetDoc(doc1) + if err != nil { + t.Fatalf("Update SetDoc failed: %v", err) + } + if doc1.Version != 2 { + t.Fatalf("Expected version 2 after content update, got %d", doc1.Version) + } + if !updatedCalled { + t.Error("OnUpdatedDoc event not triggered") + } + + // 4. 测试 SetMeta (更新元数据, 不提升版本) + err = app.SetMeta(doc1.ID, map[string]any{"title": "New Title"}) + if err != nil { + t.Fatalf("SetMeta failed: %v", err) + } + + d2, _ := app.Get(doc1.ID) + if d2.Title != "New Title" { + t.Fatalf("Expected title 'New Title', got %s", d2.Title) + } + if d2.Version != 2 { + t.Fatalf("Expected version 2 after meta update, got %d", d2.Version) + } + + // 5. 测试 带二进制内容的 SetDoc + doc2 := &Document{ + Path: "/test/binary.txt", + BinaryContent: bytes.NewReader([]byte("Binary Content")), + } + err = app.SetDoc(doc2) + if err != nil { + t.Fatalf("SetDoc with binary failed: %v", err) + } + if doc2.FilePath == "" { + t.Error("FilePath should be set for binary content") + } + if _, err := os.Stat(doc2.FilePath); os.IsNotExist(err) { + t.Error("File should be written to disk") + } + + // 6. 测试 Remove + err = app.Remove(doc1.ID) + if err != nil { + t.Fatalf("Remove failed: %v", err) + } + if !removedCalled { + t.Error("OnRemoved event not triggered") + } + + d3, _ := app.Get(doc1.ID) + if d3 != nil { + t.Error("Document should be removed") + } +} + +func BenchmarkExtractToC(b *testing.B) { + content := "# H1\n## H1.1\n### H1.1.1\n" + strings.Repeat("Some content\n", 100) + "# H2\n## H2.1\n" + for i := 0; i < b.N; i++ { + _ = ExtractToC(content) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b7e591f --- /dev/null +++ b/go.mod @@ -0,0 +1,67 @@ +module apigo.cc/go/docDB + +go 1.25.0 + +require ( + apigo.cc/go/cast v1.3.2 + apigo.cc/go/file v1.3.1 + apigo.cc/go/id v1.3.0 + apigo.cc/go/log v1.3.2 + apigo.cc/go/tableDB v0.0.0-00010101000000-000000000000 +) + +require ( + apigo.cc/go/config v1.3.0 // indirect + apigo.cc/go/crypto v1.3.0 // indirect + apigo.cc/go/db v1.3.1 // indirect + apigo.cc/go/encoding v1.3.0 // indirect + apigo.cc/go/rand v1.3.0 // indirect + apigo.cc/go/redis v1.3.0 // indirect + apigo.cc/go/safe v1.3.0 // indirect + apigo.cc/go/shell v1.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gomodule/redigo v2.0.0+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/tools v0.44.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.72.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.50.0 // indirect +) + +replace apigo.cc/go/tableDB => ../tableDB + +replace apigo.cc/go/cast => ../cast + +replace apigo.cc/go/id => ../id + +replace apigo.cc/go/file => ../file + +replace apigo.cc/go/log => ../log + +replace apigo.cc/go/document => ../document + +replace apigo.cc/go/db => ../db + +replace apigo.cc/go/config => ../config + +replace apigo.cc/go/crypto => ../crypto + +replace apigo.cc/go/encoding => ../encoding + +replace apigo.cc/go/rand => ../rand + +replace apigo.cc/go/redis => ../redis + +replace apigo.cc/go/safe => ../safe + +replace apigo.cc/go/shell => ../shell + +replace apigo.cc/go/vision => ../vision diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7129aa9 --- /dev/null +++ b/go.sum @@ -0,0 +1,72 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= +modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= +modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= +modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= +modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM= +modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=