diff --git a/src/cmd/main.go b/src/cmd/main.go index 069b750..2907ab1 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -48,9 +48,13 @@ func main() { settings.Load() raw := os.Args[1] var err error - if strings.Contains(strings.ToLower(raw), "nvm://malware") { + lower := strings.ToLower(raw) + switch { + case strings.Contains(lower, "nvm://malware"): err = malware.HandleProtocolURI(raw) - } else { + case strings.Contains(lower, "nvm://upgrade"): + err = commands.HandleUpgradeProtocolURI(raw) + default: err = firewall.HandleProtocolURI(raw) } if err != nil { diff --git a/src/commands/sync.go b/src/commands/sync.go index 0edfe4d..36f7c01 100644 --- a/src/commands/sync.go +++ b/src/commands/sync.go @@ -8,11 +8,13 @@ import ( "os" "os/exec" "path/filepath" + "strings" ) type Upgrade struct { - Check bool `flag:"check" help:"Check for updates without performing the upgrade."` - Force bool `flag:"force" help:"Upgrade without warning about processes that will be closed."` + Check bool `flag:"check" help:"Check for updates without performing the upgrade."` + Force bool `flag:"force" help:"Upgrade without warning about processes that will be closed."` + Release string `flag:"release" help:"Install this NVM version instead of the latest general or hotfix."` } func (s *Upgrade) Run() error { @@ -39,6 +41,9 @@ func (s *Upgrade) Run() error { if s.Force { cmd.Args = append(cmd.Args, "--force") } + if rel := strings.TrimSpace(s.Release); rel != "" { + cmd.Args = append(cmd.Args, "--release", rel) + } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/src/commands/upgrade_protocol.go b/src/commands/upgrade_protocol.go new file mode 100644 index 0000000..353fe7f --- /dev/null +++ b/src/commands/upgrade_protocol.go @@ -0,0 +1,41 @@ +package commands + +import ( + "fmt" + neturl "net/url" + "strings" +) + +// HandleUpgradeProtocolURI runs a toast Apply/Install/Update action +// (nvm://upgrade?version=x.y.z). Force skips the process-close prompt +// because protocol activation has no console. +func HandleUpgradeProtocolURI(raw string) error { + version, err := parseUpgradeProtocolVersion(raw) + if err != nil { + return err + } + return (&Upgrade{Force: true, Release: version}).Run() +} + +func parseUpgradeProtocolVersion(raw string) (string, error) { + raw = strings.Trim(strings.TrimSpace(raw), `"'`) + u, err := neturl.Parse(raw) + if err != nil { + return "", fmt.Errorf("invalid nvm protocol URL: %w", err) + } + if !strings.EqualFold(u.Scheme, "nvm") { + return "", fmt.Errorf("unsupported protocol %q", u.Scheme) + } + host := strings.ToLower(strings.TrimSpace(u.Host)) + if host == "" { + host = strings.ToLower(strings.Trim(u.Path, "/")) + } + if host != "upgrade" { + return "", fmt.Errorf("unsupported nvm:// host %q", host) + } + version := strings.TrimPrefix(strings.TrimSpace(u.Query().Get("version")), "v") + if version == "" { + return "", fmt.Errorf("upgrade requires version") + } + return version, nil +} diff --git a/src/commands/upgrade_protocol_test.go b/src/commands/upgrade_protocol_test.go new file mode 100644 index 0000000..e9ed166 --- /dev/null +++ b/src/commands/upgrade_protocol_test.go @@ -0,0 +1,29 @@ +package commands + +import "testing" + +func TestParseUpgradeProtocolVersion(t *testing.T) { + tests := []struct { + in, want string + ok bool + }{ + {"nvm://upgrade?version=2.1.0", "2.1.0", true}, + {"nvm://upgrade?version=v2.1.0-beta.4", "2.1.0-beta.4", true}, + {`"nvm://upgrade?version=2.0.1-hotfix.2"`, "2.0.1-hotfix.2", true}, + {"nvm://firewall?action=trust", "", false}, + {"nvm://upgrade", "", false}, + {"https://example.com", "", false}, + } + for _, tt := range tests { + got, err := parseUpgradeProtocolVersion(tt.in) + if tt.ok { + if err != nil || got != tt.want { + t.Fatalf("parseUpgradeProtocolVersion(%q) = (%q, %v), want %q", tt.in, got, err, tt.want) + } + continue + } + if err == nil { + t.Fatalf("parseUpgradeProtocolVersion(%q) = %q, want error", tt.in, got) + } + } +} diff --git a/src/test/commands/cli_t5_test.go b/src/test/commands/cli_t5_test.go index a3993e7..c8ed918 100644 --- a/src/test/commands/cli_t5_test.go +++ b/src/test/commands/cli_t5_test.go @@ -1,9 +1,12 @@ package commands_test import ( + "os" + "path/filepath" "strings" "testing" + "nvm/commands" "nvm/test/clitest" ) @@ -51,3 +54,28 @@ func TestCLIUpgradeCheckWithSyncStub(t *testing.T) { t.Fatalf("Execute(upgrade --check) error = %v stderr = %q", err, stderr) } } + +func TestCLIUpgradeProtocolDispatchesSync(t *testing.T) { + sb := clitest.NewSandbox(t) + if err := sb.WriteSetting("disable_upgrade", false); err != nil { + t.Fatalf("WriteSetting(disable_upgrade) error = %v", err) + } + + record := filepath.Join(sb.TempRoot, "sync-args.txt") + t.Setenv("NVM_RESHIM_TEST_SYNC_RECORD", record) + clitest.InstallUtilityStub(t, "sync.exe") + t.Setenv("NVM_RESHIM_TEST_SYNC_HELPER", "1") + t.Setenv("NVM_RESHIM_TEST_SYNC_EXIT_CODE", "0") + + if err := commands.HandleUpgradeProtocolURI("nvm://upgrade?version=2.1.0-beta.4"); err != nil { + t.Fatalf("HandleUpgradeProtocolURI error = %v", err) + } + raw, err := os.ReadFile(record) + if err != nil { + t.Fatalf("ReadFile(record) error = %v", err) + } + text := string(raw) + if !strings.Contains(text, "upgrade") || !strings.Contains(text, "--release") || !strings.Contains(text, "2.1.0-beta.4") || !strings.Contains(text, "--force") { + t.Fatalf("sync args = %q, want upgrade --force --release 2.1.0-beta.4", text) + } +}