| 1 | package store |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "hash/fnv" |
| 6 | "strings" |
| 7 | "unicode/utf8" |
| 8 | ) |
| 9 | |
| 10 | // Remote-SSH module naming: the canonical file names for the state a |
| 11 | // bootstrapped remote serve leaves under the remote host's |
| 12 | // ~/.reasonix/remote/. Only name derivation lives here (this package is the |
| 13 | // path authority and does no I/O); reads and writes happen over SFTP in |
| 14 | // internal/remote. Local-side absolute paths (managed known_hosts) are |
| 15 | // derived in internal/config/paths.go, which owns REASONIX_HOME resolution. |
| 16 | |
| 17 | // RemoteDirName is the directory under the remote ~/.reasonix that holds all |
| 18 | // remote-module state, and under the local Reasonix home that holds the |
| 19 | // managed known_hosts file. |
| 20 | const RemoteDirName = "remote" |
| 21 | |
| 22 | // RemoteBinDirName holds an uploaded reasonix binary on the remote host: |
| 23 | // ~/.reasonix/remote/bin/reasonix. |
| 24 | const RemoteBinDirName = "bin" |
| 25 | |
| 26 | // RemoteWorkspaceSlug flattens a remote (POSIX) workspace path into a |
| 27 | // filename component, mirroring config.WorkspaceSlug's shape. Remote targets |
| 28 | // are Linux/macOS only, so no case folding applies. A readable prefix derived |
| 29 | // from the path is always suffixed with an FNV-1a hash of the exact original |
| 30 | // path, so lossy separator replacement can never make two distinct workspaces |
| 31 | // share serve state: "/srv/a-b" and "/srv/a/b" both reduce to the readable |
| 32 | // stem "srv-a-b" but hash differently, yielding distinct slugs (and thus |
| 33 | // distinct pid/token/log/state files). |
| 34 | func RemoteWorkspaceSlug(remotePath string) string { |
| 35 | clean := strings.TrimSuffix(remotePath, "/") |
| 36 | stem := strings.NewReplacer("/", "-", ":", "-").Replace(clean) |
| 37 | stem = strings.Trim(stem, "-") |
| 38 | if stem == "" { |
| 39 | stem = "root" |
| 40 | } |
| 41 | h := fnv.New64a() |
| 42 | _, _ = h.Write([]byte(clean)) |
| 43 | sum := fmt.Sprintf("%016x", h.Sum64()) |
| 44 | // Cap the readable stem so the whole slug (stem + "-" + 16 hex) fits the |
| 45 | // per-workspace filename budget with room for the serve-<slug>.token wrapper. |
| 46 | stem = boundRemoteComponent(stem, 180) |
| 47 | return stem + "-" + sum |
| 48 | } |
| 49 | |
| 50 | // RemoteServeStateName is the per-workspace serve state JSON: pid, addr, |
| 51 | // workspace, version, started_at. |
| 52 | func RemoteServeStateName(slug string) string { return "serve-" + slug + ".json" } |
| 53 | |
| 54 | // RemoteServeTokenName holds the pre-shared auth token (0600), written over |
| 55 | // SFTP before launch and read by serve via --token-file. |
| 56 | func RemoteServeTokenName(slug string) string { return "serve-" + slug + ".token" } |
| 57 | |
| 58 | // RemoteServeLogName captures the detached serve's stdout/stderr. |
| 59 | func RemoteServeLogName(slug string) string { return "serve-" + slug + ".log" } |
| 60 | |
| 61 | // RemoteServePortName receives the real bound address via --port-file. |
| 62 | func RemoteServePortName(slug string) string { return "serve-" + slug + ".port" } |
| 63 | |
| 64 | // RemoteServePidName receives the server pid via --pid-file. |
| 65 | func RemoteServePidName(slug string) string { return "serve-" + slug + ".pid" } |
| 66 | |
| 67 | // RemoteServeLockName is the cross-client bootstrap lock directory. Directory |
| 68 | // creation is atomic on SFTP servers, including the Linux/macOS targets. |
| 69 | func RemoteServeLockName(slug string) string { return "serve-" + slug + ".lock" } |
| 70 | |
| 71 | // boundRemoteComponent mirrors config.boundFilenameComponent (this package is |
| 72 | // a stdlib-only leaf and cannot import config): inputs at or under the budget |
| 73 | // pass through byte-identical; longer ones are truncated at a rune boundary |
| 74 | // with an FNV-1a hash of the full input appended. |
| 75 | func boundRemoteComponent(s string, maxLen int) string { |
| 76 | if maxLen <= 0 || len(s) <= maxLen { |
| 77 | return s |
| 78 | } |
| 79 | h := fnv.New64a() |
| 80 | _, _ = h.Write([]byte(s)) |
| 81 | budget := maxLen - 17 // "-" + 16 hex digits |
| 82 | prefix := s[:budget] |
| 83 | for len(prefix) > 0 && !utf8.ValidString(prefix) { |
| 84 | prefix = prefix[:len(prefix)-1] |
| 85 | } |
| 86 | return fmt.Sprintf("%s-%016x", prefix, h.Sum64()) |
| 87 | } |
| 88 |