diff --git a/TODO.org b/TODO.org index 6935de6..80f41ae 100644 --- a/TODO.org +++ b/TODO.org @@ -1,5 +1,5 @@ This is a running list of things that need to taken care of at some point: - [ ] Long subtract function too complex - - [ ] Manifest requires registry key on Windows - - [ ] There has got to be a way to figure out which XCode version is installed + - [ ] The native messaging is broken on browsers installed by snap + - this is an issue with the Firefox Snapcraft in particular: https://forum.snapcraft.io/t/firefox-snapcraft-native-messaging-behavior/40437 diff --git a/cmd/cli/main.go b/cmd/cli/main.go index da324da..0e2e5cb 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -17,8 +17,14 @@ func main() { // When a browser sends a native message it will send the extension ID as the last argument. // When we see one of Armaria's extension IDs we switch to native messaging mode. extensionIds := []string{manifest.FirefoxExtension, manifest.ChromeExtension1, manifest.ChromeExtension2} - lastArg := os.Args[len(os.Args)-1] - if slices.Contains(extensionIds, lastArg) { + hostMode := false + for _, arg := range os.Args { + if slices.Contains(extensionIds, arg) { + hostMode = true + } + } + + if hostMode { if err := messaging.Dispatch(os.Stdin, os.Stdout); err != nil { fmt.Printf("Unexpected error: %s", err) os.Exit(1) diff --git a/internal/manifest/install.go b/internal/manifest/install.go index 49f5641..b8d84a0 100644 --- a/internal/manifest/install.go +++ b/internal/manifest/install.go @@ -1,66 +1,8 @@ -package manifest - -import ( - "encoding/json" - "fmt" - "os" -) - -const name = "armaria" -const description = "Armaria is a fast local-first bookmarks manager." -const hostType = "stdio" -const FirefoxExtension = "armaria@armaria.net" -const ChromeExtension1 = "chrome-extension://cahkgigfdplmhgjbioakkgennhncioli/" -const ChromeExtension2 = "chrome-extension://fbnilfpngakppdkddndcnckolmlpghdf/" +//go:build !windows -// TODO: Windows needs registry entiries. +package manifest // InstallManifest installs the app manifest. func InstallManifest(path string, hostPath string, manifestType ManifestType) error { - var err error - var buffer []byte - - if manifestType == ManifestChrome || manifestType == ManifestChromium { - manifest := chromeManifest{ - Name: name, - Description: description, - Path: hostPath, - HostType: hostType, - AllowedOrigins: []string{ - ChromeExtension1, - ChromeExtension2, - }, - } - - buffer, err = json.Marshal(manifest) - if err != nil { - return fmt.Errorf("error marshalling manifest while installing manifest: %w", err) - } - } else if manifestType == ManifestFirefox { - manifest := firefoxManifest{ - Name: name, - Description: description, - Path: hostPath, - HostType: hostType, - AllowedExtensions: []string{FirefoxExtension}, - } - - buffer, err = json.Marshal(manifest) - if err != nil { - return fmt.Errorf("error marshalling manifest while installing manifest: %w", err) - } - } - - handle, err := os.Create(path) - if err != nil { - return fmt.Errorf("error creating manifest file while installing manifest: %w", err) - } - defer handle.Close() - - _, err = handle.Write(buffer) - if err != nil { - return fmt.Errorf("error writing manfiest file contents while installing manifest: %w", err) - } - - return nil + return installManifest(path, hostPath, manifestType) } diff --git a/internal/manifest/install_common.go b/internal/manifest/install_common.go new file mode 100644 index 0000000..e7d8a21 --- /dev/null +++ b/internal/manifest/install_common.go @@ -0,0 +1,64 @@ +package manifest + +import ( + "encoding/json" + "fmt" + "os" +) + +const name = "armaria" +const description = "Armaria is a fast local-first bookmarks manager." +const hostType = "stdio" +const FirefoxExtension = "armaria@armaria.net" +const ChromeExtension1 = "chrome-extension://cahkgigfdplmhgjbioakkgennhncioli/" +const ChromeExtension2 = "chrome-extension://fbnilfpngakppdkddndcnckolmlpghdf/" + +// installManifest installs the app manifest. +func installManifest(path string, hostPath string, manifestType ManifestType) error { + var err error + var buffer []byte + + if manifestType == ManifestChrome || manifestType == ManifestChromium { + manifest := chromeManifest{ + Name: name, + Description: description, + Path: hostPath, + HostType: hostType, + AllowedOrigins: []string{ + ChromeExtension1, + ChromeExtension2, + }, + } + + buffer, err = json.Marshal(manifest) + if err != nil { + return fmt.Errorf("error marshalling manifest while installing manifest: %w", err) + } + } else if manifestType == ManifestFirefox { + manifest := firefoxManifest{ + Name: name, + Description: description, + Path: hostPath, + HostType: hostType, + AllowedExtensions: []string{FirefoxExtension}, + } + + buffer, err = json.Marshal(manifest) + if err != nil { + return fmt.Errorf("error marshalling manifest while installing manifest: %w", err) + } + } + + handle, err := os.Create(path) + if err != nil { + return fmt.Errorf("error creating manifest file while installing manifest: %w", err) + } + defer handle.Close() + + _, err = handle.Write(buffer) + if err != nil { + return fmt.Errorf("error writing manfiest file contents while installing manifest: %w", err) + } + + return nil +} diff --git a/internal/manifest/install_windows.go b/internal/manifest/install_windows.go new file mode 100644 index 0000000..c45d111 --- /dev/null +++ b/internal/manifest/install_windows.go @@ -0,0 +1,79 @@ +//go:build windows + +package manifest + +import ( + "fmt" + "strings" + + "golang.org/x/sys/windows/registry" +) + +// InstallManifest installs the app manifest. +func InstallManifest(path string, hostPath string, manifestType ManifestType) error { + err := installManifest(path, hostPath, manifestType) + if err != nil { + return err + } + + // Windows (unfortunately) requires registry entries here. + if manifestType == ManifestChrome || manifestType == ManifestChromium { + if err := writeKey(`Software\Google\Chrome\NativeMessagingHosts\armaria`, path); err != nil { + return err + } + } else if manifestType == ManifestFirefox { + if err := writeKey(`Software\Mozilla\NativeMessagingHosts\armaria`, path); err != nil { + return err + } + } + + return nil +} + +// writeKey writes value to registry key. +// It gets written to both local machine and current user. +func writeKey(path string, value string) error { + localMachineKey, err := openKey(registry.LOCAL_MACHINE, path) + if err != nil { + return fmt.Errorf("error opening local machine key while installing manifest: %w", err) + } + defer localMachineKey.Close() + + if err := localMachineKey.SetStringValue("", value); err != nil { + return fmt.Errorf("error writing local machine key while installing manifest: %w", err) + } + + userKey, err := openKey(registry.CURRENT_USER, path) + if err != nil { + return fmt.Errorf("error opening current user key while installing manifest: %w", err) + } + defer userKey.Close() + + if err := userKey.SetStringValue("", value); err != nil { + return fmt.Errorf("error writing current user key while installing manifest: %w", err) + } + + return nil +} + +// openKey opens a key for writing. +// It will create any parent keys necessary. +func openKey(root registry.Key, path string) (registry.Key, error) { + tokens := strings.Split(path, `\`) + keys := []registry.Key{root} + for _, token := range tokens { + key, _, err := registry.CreateKey(keys[len(keys)-1], token, registry.WRITE) + if err != nil { + return key, err + } + keys = append(keys, key) + } + + for i, key := range keys { + if i != 0 && i != len(keys)-1 { + key.Close() + } + } + + return keys[len(keys)-1], nil +} diff --git a/internal/paths/paths.go b/internal/paths/paths.go index 32e9fc0..af9331f 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -135,10 +135,10 @@ func snapOrHome(userHome userHomeFn, getenv getenvFn) (string, error) { // Host will get the path to the native messaging host. // The extensions need an absolute path to it in order to work. func Host() (string, error) { - return hostInternal(os.Getenv, os.Executable, filepath.Dir, filepath.Join) + return hostInternal(runtime.GOOS, os.Getenv, os.Executable, filepath.Dir, filepath.Join) } -func hostInternal(getenv getenvFn, executable executableFn, dir dirFn, join joinFn) (string, error) { +func hostInternal(goos string, getenv getenvFn, executable executableFn, dir dirFn, join joinFn) (string, error) { // The snap path needs to be a hard coded special case. // There doesn't appear to be way to intuit it. // Additionally snap will namespace the host with "armaria". @@ -154,7 +154,13 @@ func hostInternal(getenv getenvFn, executable executableFn, dir dirFn, join join if err != nil { return "", fmt.Errorf("error getting current executable while getting host path: %w", err) } - return join(dir(ex), "armaria"), nil + + hostExe := "armaria" + if goos == "windows" { + hostExe = "armaria.exe" + } + + return join(dir(ex), hostExe), nil } // FirefoxManifest gets the path to the Firefox app manifest. diff --git a/internal/paths/paths_test.go b/internal/paths/paths_test.go index 0b616dd..d68ac83 100644 --- a/internal/paths/paths_test.go +++ b/internal/paths/paths_test.go @@ -224,19 +224,27 @@ func TestGetFolderPath(t *testing.T) { func TestHostPath(t *testing.T) { type test struct { + goos string snapRealHome string hostPath string } tests := []test{ { + goos: "linux", snapRealHome: "", hostPath: "/usr/bin/armaria", }, { + goos: "linux", snapRealHome: "/snap", hostPath: "/snap/bin/armaria", }, + { + goos: "windows", + snapRealHome: "", + hostPath: "/usr/bin/armaria.exe", + }, } executable := func() (string, error) { @@ -257,7 +265,7 @@ func TestHostPath(t *testing.T) { return tc.snapRealHome } - got, err := hostInternal(getenv, executable, dir, path.Join) + got, err := hostInternal(tc.goos, getenv, executable, dir, path.Join) if err != nil { t.Fatalf("unexpected error: %+v", err) }