Files
MyGoNavi/internal/cli/connection_file.go
Syngnat 4d5c0e6bb9 feat(cli): 新增独立命令行与发布链
- 新增无头运行时及连接、查询、导出、批处理、审计和 MCP 命令
- 复用活动数据根、密文存储与跨进程锁,落实写入安全和取消语义
- 增加六平台 CLI 归档、独立校验和、Docker、npm 与 WinGet 分发
- 隔离 GUI/CLI 更新资产并强化 macOS 签名与公证门禁
- 补充并发、审计、事务及发布契约回归测试

Refs #902
2026-08-11 10:34:58 +08:00

93 lines
2.8 KiB
Go

package cli
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
stdRuntime "runtime"
"strings"
"GoNavi-Wails/internal/connection"
)
const maxConnectionFileBytes = 1 << 20
var cliGOOS = func() string {
return stdRuntime.GOOS
}
// loadTemporaryConnectionConfig reads one complete ConnectionConfig without
// touching the saved-connection repository. A connection-file is deliberately
// a raw ConnectionConfig so credentials never need to appear in argv.
func loadTemporaryConnectionConfig(path string) (connection.ConnectionConfig, error) {
path = strings.TrimSpace(path)
if path == "" {
return connection.ConnectionConfig{}, errors.New("connection file path is required")
}
entry, err := os.Lstat(path)
if err != nil {
return connection.ConnectionConfig{}, err
}
if entry.Mode()&os.ModeSymlink != 0 {
return connection.ConnectionConfig{}, errors.New("connection file must not be a symbolic link")
}
if !entry.Mode().IsRegular() {
return connection.ConnectionConfig{}, errors.New("connection file must be a regular file")
}
if entry.Size() > maxConnectionFileBytes {
return connection.ConnectionConfig{}, fmt.Errorf("connection file exceeds %d bytes", maxConnectionFileBytes)
}
file, err := os.Open(path)
if err != nil {
return connection.ConnectionConfig{}, err
}
defer file.Close()
opened, err := file.Stat()
if err != nil {
return connection.ConnectionConfig{}, err
}
if !os.SameFile(entry, opened) {
return connection.ConnectionConfig{}, errors.New("connection file changed while opening")
}
if err := validateConnectionFilePermissions(file, opened.Mode()); err != nil {
return connection.ConnectionConfig{}, err
}
decoder := json.NewDecoder(io.LimitReader(file, maxConnectionFileBytes+1))
decoder.DisallowUnknownFields()
var config connection.ConnectionConfig
if err := decoder.Decode(&config); err != nil {
return connection.ConnectionConfig{}, fmt.Errorf("decode connection file: %w", err)
}
if err := ensureOnlyOneJSONValue(decoder); err != nil {
return connection.ConnectionConfig{}, err
}
if strings.TrimSpace(config.ID) != "" {
return connection.ConnectionConfig{}, errors.New("connection file must not contain id")
}
if strings.TrimSpace(config.Type) == "" {
return connection.ConnectionConfig{}, errors.New("connection file requires type")
}
// This config must remain entirely transient, even when it contains a
// password. An empty ID also keeps runtime secret resolution away from
// connections.json and the daily-secret store.
config.ID = ""
config.SavePassword = false
return config, nil
}
func ensureOnlyOneJSONValue(decoder *json.Decoder) error {
var extra any
if err := decoder.Decode(&extra); err == io.EOF {
return nil
} else if err != nil {
return fmt.Errorf("decode connection file: %w", err)
}
return errors.New("connection file must contain exactly one JSON object")
}