Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions cli/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,28 @@ func (c *Client) do(method, path string, body interface{}) (*http.Response, erro
}

// encodeProjectPath returns the project's URL hash (first 16 hex chars of
// SHA1 of the identity key). Local projects are namespaced per machine —
// "local:{machine_id}:{path}" — so the same filesystem path on different
// machines/users maps to different projects. MUST stay byte-identical to the
// server's projects.LocalProjectKey + hashPath (server/internal/projects).
// SHA1 of the identity key). MUST stay byte-identical to the server's
// projects.Create key derivation (server/internal/projects/projects.go).
//
// The server namespaces ONLY local projects, and only those: it wraps the
// identity key as "local:{machine_id}:{path}" when MachineID != "" (set during
// `cix init` for a real filesystem project), and uses the path as-is otherwise.
// External projects (GitHub repos attached via the dashboard) have no MachineID
// and a globally-unique host_path like "github.com/owner/repo@branch", which is
// hashed bare.
//
// We mirror that split with filepath.IsAbs: local projects are always addressed
// by an ABSOLUTE filesystem path (the CLI runs filepath.Abs on cwd / -p before
// it gets here), so an absolute input is local → namespace it. A non-absolute
// input is an external project identifier (e.g. from `cix search -n
// github.com/owner/repo@branch`) → hash it bare so the hash matches the row the
// server stored. Wrapping external IDs in "local:" was a regression that made
// every `-n <external>` lookup 404.
func encodeProjectPath(path string) string {
key := "local:" + machineID() + ":" + path
key := path
if filepath.IsAbs(path) {
key = "local:" + machineID() + ":" + path
}
h := sha1.Sum([]byte(key))
return fmt.Sprintf("%x", h)[:16]
}
Expand Down
44 changes: 44 additions & 0 deletions cli/internal/client/encode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package client

import (
"crypto/sha1"
"fmt"
"testing"
)

// sha1Hex16 mirrors the server's projects.hashPath: first 16 hex chars of the
// SHA1 of the identity key. Kept local to the test so a drift in encodeProjectPath
// is caught against the server's documented formula, not against itself.
func sha1Hex16(s string) string {
h := sha1.Sum([]byte(s))
return fmt.Sprintf("%x", h)[:16]
}

// TestEncodeProjectPath_ExternalIsBare is the regression test for the prod bug
// where `cix search -n github.com/owner/repo@branch` 404'd: encodeProjectPath
// unconditionally wrapped every identity in "local:{machineID}:", but the server
// hashes EXTERNAL projects (MachineID=="") from the bare host_path. An external
// identifier is never an absolute filesystem path, so it must hash bare.
func TestEncodeProjectPath_ExternalIsBare(t *testing.T) {
ext := "github.com/MythicalGames/pf3-backend@main"
want := sha1Hex16(ext) // server: hashPath(host_path), no local: wrapper
if got := encodeProjectPath(ext); got != want {
t.Errorf("encodeProjectPath(%q) = %q, want %q (external must hash bare)", ext, got, want)
}
}

// TestEncodeProjectPath_LocalIsNamespaced confirms the local path keeps the
// per-machine "local:{machineID}:{path}" namespacing the server applies when
// MachineID != "" (absolute filesystem paths only).
func TestEncodeProjectPath_LocalIsNamespaced(t *testing.T) {
abs := "/Users/dev/go/src/example"
want := sha1Hex16("local:" + machineID() + ":" + abs)
if got := encodeProjectPath(abs); got != want {
t.Errorf("encodeProjectPath(%q) = %q, want %q (local must be namespaced)", abs, got, want)
}
// And the two regimes must not collide: the same trailing string hashed
// bare vs namespaced differs.
if encodeProjectPath(abs) == sha1Hex16(abs) {
t.Errorf("local path hashed bare — namespacing not applied")
}
}
Loading