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 } // splitOwnerName splits a smart-HTTP/SSH repo path "owner/name.git" into its // IState (owner, name) form: both without the ".git" suffix. owner may itself // contain "/" for nested namespaces (e.g. "org/team/name.git" -> owner // "org/team", name "name"); name is always the final segment. IState // reconstructs the on-disk path / DB full-name as owner+"/"+name+".git". 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 } // parseDBURI maps a URI scheme to (driver, dsn), following simpleconsole's // ParseDBURI convention so the same URIs work across both services. Unexported: // Open is the only entry point callers need. // // sqlite://[?_pragma=...] -> driver "sqlite3", modernc DSN "file:?..." // postgres:// -> driver "postgres", dsn passed through verbatim 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": // lib/pq parses the URL form natively; pass it through unchanged. return "postgres", uri, uri, nil default: return "", "", "", fmt.Errorf("unsupported database URI scheme %q (want sqlite:// or postgres://)", scheme) } } // sqliteDSN builds a modernc DSN: "file:?". Default pragmas (WAL, // busy_timeout, foreign_keys) and _txlock=immediate are applied only when the // caller supplied no pragmas of their own; an explicit pragma set is respected // verbatim. 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() } // sqlitePath extracts the file path from a modernc DSN ("file:?..."). func sqlitePath(dsn string) string { s := strings.TrimPrefix(dsn, "file:") if i := strings.Index(s, "?"); i >= 0 { s = s[:i] } return s } // splitScheme splits "scheme:rest" at the first colon. ok is false when there // is no colon. 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 }