package common import ( "errors" "fmt" "net/url" "os" "path/filepath" "strings" ) func Validate(name string) error { if name == "" { return errors.New("name cannot be empty") } if strings.HasPrefix(name, "/") || strings.Contains(name, "\\") { return errors.New("name must be a relative unix-style path") } if !strings.HasSuffix(name, ".git") { return errors.New("name must end with .git") } parts := strings.Split(name, "/") for _, part := range parts { if part == "" || part == "." || part == ".." { return errors.New("name contains an invalid path segment") } } return nil } func Resolve(root, name string) (string, error) { if err := Validate(name); err != nil { return "", err } full, err := filepath.Abs(filepath.Join(root, name)) if err != nil { return "", err } absRoot, err := filepath.Abs(root) if err != nil { return "", err } if full != absRoot && !strings.HasPrefix(full, absRoot+string(os.PathSeparator)) { return "", errors.New("repository path escapes root directory") } return full, nil } func SplitRepo(repo string) (owner, name string, err error) { s := strings.TrimSuffix(repo, ".git") i := strings.LastIndex(s, "/") if i < 0 { return "", "", errors.New("repository path must be owner/name.git") } return s[:i], s[i+1:], nil } func ParseDBURI(root, uri string) (driver, dsn string, uriNew string, err error) { if uri == "" { return "", "", "", fmt.Errorf("database URI is empty") } scheme, rest, ok := splitScheme(uri) if !ok { return "", "", "", fmt.Errorf("database URI %q has no scheme (want sqlite:// or postgres://)", uri) } switch scheme { case "sqlite", "sqlite3": path, query, _ := strings.Cut(strings.TrimPrefix(rest, "//"), "?") if path == "" { return "", "", "", fmt.Errorf("sqlite URI %q has no file path", uri) } uriNew := "sqlite3://" + filepath.Join(root, path) return "sqlite3", sqliteDSN(filepath.Join(root, path), query), uriNew, nil case "postgres", "postgresql": return "postgres", uri, uri, nil default: return "", "", "", fmt.Errorf("unsupported database URI scheme %q (want sqlite:// or postgres://)", scheme) } } func sqliteDSN(path, query string) string { v, _ := url.ParseQuery(query) if len(v["_pragma"]) == 0 { v.Add("_pragma", "journal_mode(WAL)") v.Add("_pragma", "busy_timeout(5000)") v.Add("_pragma", "foreign_keys(ON)") } v.Add("_txlock", "immediate") return "file:" + path + "?" + v.Encode() } func sqlitePath(dsn string) string { s := strings.TrimPrefix(dsn, "file:") if i := strings.Index(s, "?"); i >= 0 { s = s[:i] } return s } func splitScheme(uri string) (scheme, rest string, ok bool) { i := strings.Index(uri, ":") if i < 0 { return "", uri, false } return uri[:i], uri[i+1:], true }