From 5ba151da470f751323df13da66e6dcd2919de4f4 Mon Sep 17 00:00:00 2001
From: Cristian Maglie <c.maglie@bug.st>
Date: Fri, 27 Jan 2023 13:24:34 +0100
Subject: [PATCH 1/6] Moved updater business logic in updater package

---
 main.go            | 29 ++---------------------------
 updater/updater.go | 36 ++++++++++++++++++++++++++++++++++--
 2 files changed, 36 insertions(+), 29 deletions(-)

diff --git a/main.go b/main.go
index 1cd386e07..f6bc3afd6 100755
--- a/main.go
+++ b/main.go
@@ -22,7 +22,6 @@ import (
 	_ "embed"
 	"encoding/json"
 	"flag"
-	"io/ioutil"
 	"os"
 	"runtime"
 	"runtime/debug"
@@ -149,39 +148,15 @@ func main() {
 		ConfigDir:        configDir,
 	}
 
-	// If the executable is temporary, copy it to the full path, then restart
 	if src, err := os.Executable(); err != nil {
 		panic(err)
-	} else if strings.Contains(src, "-temp") {
-		newPath := updater.RemoveTempSuffixFromPath(src)
-		if err := copyExe(src, newPath); err != nil {
-			log.Println("Copy error: ", err)
-			panic(err)
-		}
-		Systray.RestartWith(newPath)
+	} else if restartPath := updater.Start(src); restartPath != "" {
+		Systray.RestartWith(restartPath)
 	} else {
-		// Otherwise copy to a path with -temp suffix
-		if err := copyExe(src, updater.AddTempSuffixToPath(src)); err != nil {
-			panic(err)
-		}
 		Systray.Start()
 	}
 }
 
-func copyExe(from, to string) error {
-	data, err := ioutil.ReadFile(from)
-	if err != nil {
-		log.Println("Cannot read file: ", from)
-		return err
-	}
-	err = ioutil.WriteFile(to, data, 0755)
-	if err != nil {
-		log.Println("Cannot write file: ", to)
-		return err
-	}
-	return nil
-}
-
 func loop() {
 	if *hibernate {
 		return
diff --git a/updater/updater.go b/updater/updater.go
index bfffe601d..044cf091f 100644
--- a/updater/updater.go
+++ b/updater/updater.go
@@ -68,6 +68,38 @@ var errHashMismatch = errors.New("new file hash mismatch after patch")
 var errDiffURLUndefined = errors.New("DiffURL is not defined, I cannot fetch and apply patch, reverting to full bin")
 var up = update.New()
 
+func Start(src string) string {
+	// If the executable is temporary, copy it to the full path, then restart
+	if strings.Contains(src, "-temp") {
+		newPath := removeTempSuffixFromPath(src)
+		if err := copyExe(src, newPath); err != nil {
+			log.Println("Copy error: ", err)
+			panic(err)
+		}
+		return newPath
+	}
+
+	// Otherwise copy to a path with -temp suffix
+	if err := copyExe(src, AddTempSuffixToPath(src)); err != nil {
+		panic(err)
+	}
+	return ""
+}
+
+func copyExe(from, to string) error {
+	data, err := os.ReadFile(from)
+	if err != nil {
+		log.Println("Cannot read file: ", from)
+		return err
+	}
+	err = os.WriteFile(to, data, 0755)
+	if err != nil {
+		log.Println("Cannot write file: ", to)
+		return err
+	}
+	return nil
+}
+
 // AddTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
 func AddTempSuffixToPath(path string) string {
 	if filepath.Ext(path) == "exe" {
@@ -79,8 +111,8 @@ func AddTempSuffixToPath(path string) string {
 	return path
 }
 
-// RemoveTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
-func RemoveTempSuffixFromPath(path string) string {
+// removeTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
+func removeTempSuffixFromPath(path string) string {
 	return strings.Replace(path, "-temp", "", -1)
 }
 

From 7ecb5ae7161286eb071275cc927d0fd87d227eba Mon Sep 17 00:00:00 2001
From: Cristian Maglie <c.maglie@bug.st>
Date: Fri, 27 Jan 2023 13:32:23 +0100
Subject: [PATCH 2/6] Moved updater business logic in updater package

---
 update.go          | 28 ++--------------------------
 updater/updater.go | 28 ++++++++++++++++++++++++----
 2 files changed, 26 insertions(+), 30 deletions(-)

diff --git a/update.go b/update.go
index 377f5e353..21569f36c 100644
--- a/update.go
+++ b/update.go
@@ -30,40 +30,16 @@
 package main
 
 import (
-	"os"
-
 	"github.com/arduino/arduino-create-agent/updater"
 	"github.com/gin-gonic/gin"
 )
 
 func updateHandler(c *gin.Context) {
-
-	path, err := os.Executable()
-
-	if err != nil {
-		c.JSON(500, gin.H{"error": err.Error()})
-		return
-	}
-
-	var up = &updater.Updater{
-		CurrentVersion: version,
-		APIURL:         *updateURL,
-		BinURL:         *updateURL,
-		DiffURL:        "",
-		Dir:            "update/",
-		CmdName:        *appName,
-	}
-
-	err = up.BackgroundRun()
-
+	restartPath, err := updater.CheckForUpdates(version, *updateURL, *updateURL, *appName)
 	if err != nil {
 		c.JSON(500, gin.H{"error": err.Error()})
 		return
 	}
-
-	path = updater.AddTempSuffixToPath(path)
-
 	c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"})
-
-	Systray.RestartWith(path)
+	Systray.RestartWith(restartPath)
 }
diff --git a/updater/updater.go b/updater/updater.go
index 044cf091f..4881942c4 100644
--- a/updater/updater.go
+++ b/updater/updater.go
@@ -80,12 +80,32 @@ func Start(src string) string {
 	}
 
 	// Otherwise copy to a path with -temp suffix
-	if err := copyExe(src, AddTempSuffixToPath(src)); err != nil {
+	if err := copyExe(src, addTempSuffixToPath(src)); err != nil {
 		panic(err)
 	}
 	return ""
 }
 
+func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
+	path, err := os.Executable()
+	if err != nil {
+		return "", err
+	}
+	var up = &Updater{
+		CurrentVersion: currentVersion,
+		APIURL:         updateAPIURL,
+		BinURL:         updateBinURL,
+		DiffURL:        "",
+		Dir:            "update/",
+		CmdName:        cmdName,
+	}
+
+	if err := up.BackgroundRun(); err != nil {
+		return "", err
+	}
+	return addTempSuffixToPath(path), nil
+}
+
 func copyExe(from, to string) error {
 	data, err := os.ReadFile(from)
 	if err != nil {
@@ -100,8 +120,8 @@ func copyExe(from, to string) error {
 	return nil
 }
 
-// AddTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
-func AddTempSuffixToPath(path string) string {
+// addTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
+func addTempSuffixToPath(path string) string {
 	if filepath.Ext(path) == "exe" {
 		path = strings.Replace(path, ".exe", "-temp.exe", -1)
 	} else {
@@ -266,7 +286,7 @@ func (u *Updater) update() error {
 		return err
 	}
 
-	path = AddTempSuffixToPath(path)
+	path = addTempSuffixToPath(path)
 
 	old, err := os.Open(path)
 	if err != nil {

From f36e3891d07cc527ffd76b095fc0b13fd641bf2c Mon Sep 17 00:00:00 2001
From: Cristian Maglie <c.maglie@bug.st>
Date: Fri, 27 Jan 2023 14:07:55 +0100
Subject: [PATCH 3/6] Added infrastructure for generic updater procedure

---
 updater/updater.go         | 327 +----------------------------------
 updater/updater_default.go | 344 +++++++++++++++++++++++++++++++++++++
 updater/updater_macos.go   |  26 +++
 3 files changed, 377 insertions(+), 320 deletions(-)
 create mode 100644 updater/updater_default.go
 create mode 100644 updater/updater_macos.go

diff --git a/updater/updater.go b/updater/updater.go
index 4881942c4..0308927d6 100644
--- a/updater/updater.go
+++ b/updater/updater.go
@@ -15,328 +15,15 @@
 
 package updater
 
-import (
-	"bytes"
-	"compress/gzip"
-	"crypto/sha256"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"path/filepath"
-	"runtime"
-	"strings"
-
-	"github.com/kr/binarydist"
-	log "github.com/sirupsen/logrus"
-	"gopkg.in/inconshreveable/go-update.v0"
-)
-
-// Update protocol:
-//
-//   GET hk.heroku.com/hk/linux-amd64.json
-//
-//   200 ok
-//   {
-//       "Version": "2",
-//       "Sha256": "..." // base64
-//   }
-//
-// then
-//
-//   GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64
-//
-//   200 ok
-//   [bsdiff data]
-//
-// or
-//
-//   GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz
-//
-//   200 ok
-//   [gzipped executable data]
-//
-//
-
-const (
-	plat = runtime.GOOS + "-" + runtime.GOARCH
-)
-
-var errHashMismatch = errors.New("new file hash mismatch after patch")
-var errDiffURLUndefined = errors.New("DiffURL is not defined, I cannot fetch and apply patch, reverting to full bin")
-var up = update.New()
-
+// Start checks if an update has been downloaded and if so returns the path to the
+// binary to be executed to perform the update. If no update has been downloaded
+// it returns an empty string.
 func Start(src string) string {
-	// If the executable is temporary, copy it to the full path, then restart
-	if strings.Contains(src, "-temp") {
-		newPath := removeTempSuffixFromPath(src)
-		if err := copyExe(src, newPath); err != nil {
-			log.Println("Copy error: ", err)
-			panic(err)
-		}
-		return newPath
-	}
-
-	// Otherwise copy to a path with -temp suffix
-	if err := copyExe(src, addTempSuffixToPath(src)); err != nil {
-		panic(err)
-	}
-	return ""
+	return start(src)
 }
 
+// CheckForUpdates checks if there is a new version of the binary available and
+// if so downloads it.
 func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
-	path, err := os.Executable()
-	if err != nil {
-		return "", err
-	}
-	var up = &Updater{
-		CurrentVersion: currentVersion,
-		APIURL:         updateAPIURL,
-		BinURL:         updateBinURL,
-		DiffURL:        "",
-		Dir:            "update/",
-		CmdName:        cmdName,
-	}
-
-	if err := up.BackgroundRun(); err != nil {
-		return "", err
-	}
-	return addTempSuffixToPath(path), nil
-}
-
-func copyExe(from, to string) error {
-	data, err := os.ReadFile(from)
-	if err != nil {
-		log.Println("Cannot read file: ", from)
-		return err
-	}
-	err = os.WriteFile(to, data, 0755)
-	if err != nil {
-		log.Println("Cannot write file: ", to)
-		return err
-	}
-	return nil
-}
-
-// addTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
-func addTempSuffixToPath(path string) string {
-	if filepath.Ext(path) == "exe" {
-		path = strings.Replace(path, ".exe", "-temp.exe", -1)
-	} else {
-		path = path + "-temp"
-	}
-
-	return path
-}
-
-// removeTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
-func removeTempSuffixFromPath(path string) string {
-	return strings.Replace(path, "-temp", "", -1)
-}
-
-// Updater is the configuration and runtime data for doing an update.
-//
-// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
-//
-// Example:
-//
-//	updater := &selfupdate.Updater{
-//		CurrentVersion: version,
-//		ApiURL:         "http://updates.yourdomain.com/",
-//		BinURL:         "http://updates.yourdownmain.com/",
-//		DiffURL:        "http://updates.yourdomain.com/",
-//		Dir:            "update/",
-//		CmdName:        "myapp", // app name
-//	}
-//	if updater != nil {
-//		go updater.BackgroundRun()
-//	}
-type Updater struct {
-	CurrentVersion string // Currently running version.
-	APIURL         string // Base URL for API requests (json files).
-	CmdName        string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
-	BinURL         string // Base URL for full binary downloads.
-	DiffURL        string // Base URL for diff downloads.
-	Dir            string // Directory to store selfupdate state.
-	Info           struct {
-		Version string
-		Sha256  []byte
-	}
-}
-
-// BackgroundRun starts the update check and apply cycle.
-func (u *Updater) BackgroundRun() error {
-	os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777)
-	if err := up.CanUpdate(); err != nil {
-		log.Println(err)
-		return err
-	}
-	//self, err := os.Executable()
-	//if err != nil {
-	// fail update, couldn't figure out path to self
-	//return
-	//}
-	// TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports.
-	if err := u.update(); err != nil {
-		return err
-	}
-	return nil
-}
-
-func fetch(url string) (io.ReadCloser, error) {
-	resp, err := http.Get(url)
-	if err != nil {
-		return nil, err
-	}
-	if resp.StatusCode != 200 {
-		log.Errorf("bad http status from %s: %v", url, resp.Status)
-		return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
-	}
-	return resp.Body, nil
-}
-
-func verifySha(bin []byte, sha []byte) bool {
-	h := sha256.New()
-	h.Write(bin)
-	return bytes.Equal(h.Sum(nil), sha)
-}
-
-func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
-	if u.DiffURL == "" {
-		return nil, errDiffURLUndefined
-	}
-	r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat)
-	if err != nil {
-		return nil, err
-	}
-	defer r.Close()
-	var buf bytes.Buffer
-	err = binarydist.Patch(old, &buf, r)
-	return buf.Bytes(), err
-}
-
-func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
-	bin, err := u.fetchAndApplyPatch(old)
-	if err != nil {
-		return nil, err
-	}
-	if !verifySha(bin, u.Info.Sha256) {
-		return nil, errHashMismatch
-	}
-	return bin, nil
-}
-
-func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
-	bin, err := u.fetchBin()
-	if err != nil {
-		return nil, err
-	}
-	verified := verifySha(bin, u.Info.Sha256)
-	if !verified {
-		return nil, errHashMismatch
-	}
-	return bin, nil
-}
-
-func (u *Updater) fetchBin() ([]byte, error) {
-	r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz")
-	if err != nil {
-		return nil, err
-	}
-	defer r.Close()
-	buf := new(bytes.Buffer)
-	gz, err := gzip.NewReader(r)
-	if err != nil {
-		return nil, err
-	}
-	if _, err = io.Copy(buf, gz); err != nil {
-		return nil, err
-	}
-
-	return buf.Bytes(), nil
-}
-
-func (u *Updater) fetchInfo() error {
-	r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json")
-	if err != nil {
-		return err
-	}
-	defer r.Close()
-	err = json.NewDecoder(r).Decode(&u.Info)
-	if err != nil {
-		return err
-	}
-	if len(u.Info.Sha256) != sha256.Size {
-		return errors.New("bad cmd hash in info")
-	}
-	return nil
-}
-
-func (u *Updater) getExecRelativeDir(dir string) string {
-	filename, _ := os.Executable()
-	path := filepath.Join(filepath.Dir(filename), dir)
-	return path
-}
-
-func (u *Updater) update() error {
-	path, err := os.Executable()
-	if err != nil {
-		return err
-	}
-
-	path = addTempSuffixToPath(path)
-
-	old, err := os.Open(path)
-	if err != nil {
-		return err
-	}
-	defer old.Close()
-
-	err = u.fetchInfo()
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-	if u.Info.Version == u.CurrentVersion {
-		return nil
-	}
-	bin, err := u.fetchAndVerifyPatch(old)
-	if err != nil {
-		switch err {
-		case errHashMismatch:
-			log.Println("update: hash mismatch from patched binary")
-		case errDiffURLUndefined:
-			log.Println("update: ", err)
-		default:
-			log.Println("update: patching binary, ", err)
-		}
-
-		bin, err = u.fetchAndVerifyFullBin()
-		if err != nil {
-			if err == errHashMismatch {
-				log.Println("update: hash mismatch from full binary")
-			} else {
-				log.Println("update: fetching full binary,", err)
-			}
-			return err
-		}
-	}
-
-	// close the old binary before installing because on windows
-	// it can't be renamed if a handle to the file is still open
-	old.Close()
-
-	up.TargetPath = path
-	err, errRecover := up.FromStream(bytes.NewBuffer(bin))
-	if errRecover != nil {
-		log.Errorf("update and recovery errors: %q %q", err, errRecover)
-		return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
-	}
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return checkForUpdates(currentVersion, updateAPIURL, updateBinURL, cmdName)
 }
diff --git a/updater/updater_default.go b/updater/updater_default.go
new file mode 100644
index 000000000..7ab71dcf0
--- /dev/null
+++ b/updater/updater_default.go
@@ -0,0 +1,344 @@
+// Copyright 2022 Arduino SA
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//go:build !darwin
+
+package updater
+
+import (
+	"bytes"
+	"compress/gzip"
+	"crypto/sha256"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/kr/binarydist"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/inconshreveable/go-update.v0"
+)
+
+// Update protocol:
+//
+//   GET hk.heroku.com/hk/linux-amd64.json
+//
+//   200 ok
+//   {
+//       "Version": "2",
+//       "Sha256": "..." // base64
+//   }
+//
+// then
+//
+//   GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64
+//
+//   200 ok
+//   [bsdiff data]
+//
+// or
+//
+//   GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz
+//
+//   200 ok
+//   [gzipped executable data]
+//
+//
+
+const (
+	plat = runtime.GOOS + "-" + runtime.GOARCH
+)
+
+var errHashMismatch = errors.New("new file hash mismatch after patch")
+var errDiffURLUndefined = errors.New("DiffURL is not defined, I cannot fetch and apply patch, reverting to full bin")
+var up = update.New()
+
+func start(src string) string {
+	// If the executable is temporary, copy it to the full path, then restart
+	if strings.Contains(src, "-temp") {
+		newPath := removeTempSuffixFromPath(src)
+		if err := copyExe(src, newPath); err != nil {
+			log.Println("Copy error: ", err)
+			panic(err)
+		}
+		return newPath
+	}
+
+	// Otherwise copy to a path with -temp suffix
+	if err := copyExe(src, addTempSuffixToPath(src)); err != nil {
+		panic(err)
+	}
+	return ""
+}
+
+func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
+	path, err := os.Executable()
+	if err != nil {
+		return "", err
+	}
+	var up = &Updater{
+		CurrentVersion: currentVersion,
+		APIURL:         updateAPIURL,
+		BinURL:         updateBinURL,
+		DiffURL:        "",
+		Dir:            "update/",
+		CmdName:        cmdName,
+	}
+
+	if err := up.BackgroundRun(); err != nil {
+		return "", err
+	}
+	return addTempSuffixToPath(path), nil
+}
+
+func copyExe(from, to string) error {
+	data, err := os.ReadFile(from)
+	if err != nil {
+		log.Println("Cannot read file: ", from)
+		return err
+	}
+	err = os.WriteFile(to, data, 0755)
+	if err != nil {
+		log.Println("Cannot write file: ", to)
+		return err
+	}
+	return nil
+}
+
+// addTempSuffixToPath adds the "-temp" suffix to the path to an executable file (a ".exe" extension is replaced with "-temp.exe")
+func addTempSuffixToPath(path string) string {
+	if filepath.Ext(path) == "exe" {
+		path = strings.Replace(path, ".exe", "-temp.exe", -1)
+	} else {
+		path = path + "-temp"
+	}
+
+	return path
+}
+
+// removeTempSuffixFromPath removes "-temp" suffix from the path to an executable file (a "-temp.exe" extension is replaced with ".exe")
+func removeTempSuffixFromPath(path string) string {
+	return strings.Replace(path, "-temp", "", -1)
+}
+
+// Updater is the configuration and runtime data for doing an update.
+//
+// Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
+//
+// Example:
+//
+//	updater := &selfupdate.Updater{
+//		CurrentVersion: version,
+//		ApiURL:         "http://updates.yourdomain.com/",
+//		BinURL:         "http://updates.yourdownmain.com/",
+//		DiffURL:        "http://updates.yourdomain.com/",
+//		Dir:            "update/",
+//		CmdName:        "myapp", // app name
+//	}
+//	if updater != nil {
+//		go updater.BackgroundRun()
+//	}
+type Updater struct {
+	CurrentVersion string // Currently running version.
+	APIURL         string // Base URL for API requests (json files).
+	CmdName        string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
+	BinURL         string // Base URL for full binary downloads.
+	DiffURL        string // Base URL for diff downloads.
+	Dir            string // Directory to store selfupdate state.
+	Info           struct {
+		Version string
+		Sha256  []byte
+	}
+}
+
+// BackgroundRun starts the update check and apply cycle.
+func (u *Updater) BackgroundRun() error {
+	os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777)
+	if err := up.CanUpdate(); err != nil {
+		log.Println(err)
+		return err
+	}
+	//self, err := os.Executable()
+	//if err != nil {
+	// fail update, couldn't figure out path to self
+	//return
+	//}
+	// TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports.
+	if err := u.update(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func fetch(url string) (io.ReadCloser, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != 200 {
+		log.Errorf("bad http status from %s: %v", url, resp.Status)
+		return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
+	}
+	return resp.Body, nil
+}
+
+func verifySha(bin []byte, sha []byte) bool {
+	h := sha256.New()
+	h.Write(bin)
+	return bytes.Equal(h.Sum(nil), sha)
+}
+
+func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
+	if u.DiffURL == "" {
+		return nil, errDiffURLUndefined
+	}
+	r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+	var buf bytes.Buffer
+	err = binarydist.Patch(old, &buf, r)
+	return buf.Bytes(), err
+}
+
+func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
+	bin, err := u.fetchAndApplyPatch(old)
+	if err != nil {
+		return nil, err
+	}
+	if !verifySha(bin, u.Info.Sha256) {
+		return nil, errHashMismatch
+	}
+	return bin, nil
+}
+
+func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
+	bin, err := u.fetchBin()
+	if err != nil {
+		return nil, err
+	}
+	verified := verifySha(bin, u.Info.Sha256)
+	if !verified {
+		return nil, errHashMismatch
+	}
+	return bin, nil
+}
+
+func (u *Updater) fetchBin() ([]byte, error) {
+	r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz")
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+	buf := new(bytes.Buffer)
+	gz, err := gzip.NewReader(r)
+	if err != nil {
+		return nil, err
+	}
+	if _, err = io.Copy(buf, gz); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
+func (u *Updater) fetchInfo() error {
+	r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json")
+	if err != nil {
+		return err
+	}
+	defer r.Close()
+	err = json.NewDecoder(r).Decode(&u.Info)
+	if err != nil {
+		return err
+	}
+	if len(u.Info.Sha256) != sha256.Size {
+		return errors.New("bad cmd hash in info")
+	}
+	return nil
+}
+
+func (u *Updater) getExecRelativeDir(dir string) string {
+	filename, _ := os.Executable()
+	path := filepath.Join(filepath.Dir(filename), dir)
+	return path
+}
+
+func (u *Updater) update() error {
+	path, err := os.Executable()
+	if err != nil {
+		return err
+	}
+
+	path = addTempSuffixToPath(path)
+
+	old, err := os.Open(path)
+	if err != nil {
+		return err
+	}
+	defer old.Close()
+
+	err = u.fetchInfo()
+	if err != nil {
+		log.Println(err)
+		return err
+	}
+	if u.Info.Version == u.CurrentVersion {
+		return nil
+	}
+	bin, err := u.fetchAndVerifyPatch(old)
+	if err != nil {
+		switch err {
+		case errHashMismatch:
+			log.Println("update: hash mismatch from patched binary")
+		case errDiffURLUndefined:
+			log.Println("update: ", err)
+		default:
+			log.Println("update: patching binary, ", err)
+		}
+
+		bin, err = u.fetchAndVerifyFullBin()
+		if err != nil {
+			if err == errHashMismatch {
+				log.Println("update: hash mismatch from full binary")
+			} else {
+				log.Println("update: fetching full binary,", err)
+			}
+			return err
+		}
+	}
+
+	// close the old binary before installing because on windows
+	// it can't be renamed if a handle to the file is still open
+	old.Close()
+
+	up.TargetPath = path
+	err, errRecover := up.FromStream(bytes.NewBuffer(bin))
+	if errRecover != nil {
+		log.Errorf("update and recovery errors: %q %q", err, errRecover)
+		return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
+	}
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/updater/updater_macos.go b/updater/updater_macos.go
new file mode 100644
index 000000000..a4a8b5c9e
--- /dev/null
+++ b/updater/updater_macos.go
@@ -0,0 +1,26 @@
+// Copyright 2022 Arduino SA
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+//go:build darwin
+
+package updater
+
+func start(src string) string {
+	return ""
+}
+
+func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
+	return "", nil
+}

From 0aafda140b84fbc2f4fb7c0e039a4f3b910f0ddd Mon Sep 17 00:00:00 2001
From: Cristian Maglie <c.maglie@bug.st>
Date: Fri, 27 Jan 2023 14:19:06 +0100
Subject: [PATCH 4/6] Factored fetchInfo function to retrieve information about
 the latest update

---
 updater/updater.go         | 50 ++++++++++++++++++++++++++++++++++++++
 updater/updater_default.go | 47 +++++++----------------------------
 2 files changed, 59 insertions(+), 38 deletions(-)

diff --git a/updater/updater.go b/updater/updater.go
index 0308927d6..b15afe3f8 100644
--- a/updater/updater.go
+++ b/updater/updater.go
@@ -15,6 +15,18 @@
 
 package updater
 
+import (
+	"crypto/sha256"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"runtime"
+
+	log "github.com/sirupsen/logrus"
+)
+
 // Start checks if an update has been downloaded and if so returns the path to the
 // binary to be executed to perform the update. If no update has been downloaded
 // it returns an empty string.
@@ -27,3 +39,41 @@ func Start(src string) string {
 func CheckForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) {
 	return checkForUpdates(currentVersion, updateAPIURL, updateBinURL, cmdName)
 }
+
+const (
+	plat = runtime.GOOS + "-" + runtime.GOARCH
+)
+
+func fetchInfo(updateAPIURL string, cmdName string) (*availableUpdateInfo, error) {
+	r, err := fetch(updateAPIURL + cmdName + "/" + plat + ".json")
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+
+	var res availableUpdateInfo
+	if err := json.NewDecoder(r).Decode(&res); err != nil {
+		return nil, err
+	}
+	if len(res.Sha256) != sha256.Size {
+		return nil, errors.New("bad cmd hash in info")
+	}
+	return &res, nil
+}
+
+type availableUpdateInfo struct {
+	Version string
+	Sha256  []byte
+}
+
+func fetch(url string) (io.ReadCloser, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != 200 {
+		log.Errorf("bad http status from %s: %v", url, resp.Status)
+		return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
+	}
+	return resp.Body, nil
+}
diff --git a/updater/updater_default.go b/updater/updater_default.go
index 7ab71dcf0..57dbd8187 100644
--- a/updater/updater_default.go
+++ b/updater/updater_default.go
@@ -21,14 +21,11 @@ import (
 	"bytes"
 	"compress/gzip"
 	"crypto/sha256"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
-	"net/http"
 	"os"
 	"path/filepath"
-	"runtime"
 	"strings"
 
 	"github.com/kr/binarydist"
@@ -62,10 +59,6 @@ import (
 //
 //
 
-const (
-	plat = runtime.GOOS + "-" + runtime.GOARCH
-)
-
 var errHashMismatch = errors.New("new file hash mismatch after patch")
 var errDiffURLUndefined = errors.New("DiffURL is not defined, I cannot fetch and apply patch, reverting to full bin")
 var up = update.New()
@@ -156,16 +149,13 @@ func removeTempSuffixFromPath(path string) string {
 //		go updater.BackgroundRun()
 //	}
 type Updater struct {
-	CurrentVersion string // Currently running version.
-	APIURL         string // Base URL for API requests (json files).
-	CmdName        string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
-	BinURL         string // Base URL for full binary downloads.
-	DiffURL        string // Base URL for diff downloads.
-	Dir            string // Directory to store selfupdate state.
-	Info           struct {
-		Version string
-		Sha256  []byte
-	}
+	CurrentVersion string               // Currently running version.
+	APIURL         string               // Base URL for API requests (json files).
+	CmdName        string               // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
+	BinURL         string               // Base URL for full binary downloads.
+	DiffURL        string               // Base URL for diff downloads.
+	Dir            string               // Directory to store selfupdate state.
+	Info           *availableUpdateInfo // Information about the available update.
 }
 
 // BackgroundRun starts the update check and apply cycle.
@@ -187,18 +177,6 @@ func (u *Updater) BackgroundRun() error {
 	return nil
 }
 
-func fetch(url string) (io.ReadCloser, error) {
-	resp, err := http.Get(url)
-	if err != nil {
-		return nil, err
-	}
-	if resp.StatusCode != 200 {
-		log.Errorf("bad http status from %s: %v", url, resp.Status)
-		return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
-	}
-	return resp.Body, nil
-}
-
 func verifySha(bin []byte, sha []byte) bool {
 	h := sha256.New()
 	h.Write(bin)
@@ -261,18 +239,11 @@ func (u *Updater) fetchBin() ([]byte, error) {
 }
 
 func (u *Updater) fetchInfo() error {
-	r, err := fetch(u.APIURL + u.CmdName + "/" + plat + ".json")
+	info, err := fetchInfo(u.APIURL, u.CmdName)
 	if err != nil {
 		return err
 	}
-	defer r.Close()
-	err = json.NewDecoder(r).Decode(&u.Info)
-	if err != nil {
-		return err
-	}
-	if len(u.Info.Sha256) != sha256.Size {
-		return errors.New("bad cmd hash in info")
-	}
+	u.Info = info
 	return nil
 }
 

From 5ae57a40317c9cdb663ff17e86a3bc41404e8db8 Mon Sep 17 00:00:00 2001
From: Cristian Maglie <c.maglie@bug.st>
Date: Mon, 13 Feb 2023 10:35:30 +0100
Subject: [PATCH 5/6] Inlined Updater.fetchInfo method

---
 updater/updater_default.go | 12 ++----------
 1 file changed, 2 insertions(+), 10 deletions(-)

diff --git a/updater/updater_default.go b/updater/updater_default.go
index 57dbd8187..4562907fc 100644
--- a/updater/updater_default.go
+++ b/updater/updater_default.go
@@ -238,15 +238,6 @@ func (u *Updater) fetchBin() ([]byte, error) {
 	return buf.Bytes(), nil
 }
 
-func (u *Updater) fetchInfo() error {
-	info, err := fetchInfo(u.APIURL, u.CmdName)
-	if err != nil {
-		return err
-	}
-	u.Info = info
-	return nil
-}
-
 func (u *Updater) getExecRelativeDir(dir string) string {
 	filename, _ := os.Executable()
 	path := filepath.Join(filepath.Dir(filename), dir)
@@ -267,11 +258,12 @@ func (u *Updater) update() error {
 	}
 	defer old.Close()
 
-	err = u.fetchInfo()
+	info, err := fetchInfo(u.APIURL, u.CmdName)
 	if err != nil {
 		log.Println(err)
 		return err
 	}
+	u.Info = info
 	if u.Info.Version == u.CurrentVersion {
 		return nil
 	}

From 381507224cd9b77b58d5cb9f7ab38ed23080e2a7 Mon Sep 17 00:00:00 2001
From: Cristian Maglie <c.maglie@bug.st>
Date: Mon, 13 Feb 2023 10:36:43 +0100
Subject: [PATCH 6/6] Renamed updater_macos -> updater_darwin and removed build
 flag

---
 updater/{updater_macos.go => updater_darwin.go} | 2 --
 1 file changed, 2 deletions(-)
 rename updater/{updater_macos.go => updater_darwin.go} (97%)

diff --git a/updater/updater_macos.go b/updater/updater_darwin.go
similarity index 97%
rename from updater/updater_macos.go
rename to updater/updater_darwin.go
index a4a8b5c9e..45c67e5c7 100644
--- a/updater/updater_macos.go
+++ b/updater/updater_darwin.go
@@ -13,8 +13,6 @@
 // You should have received a copy of the GNU Affero General Public License
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-//go:build darwin
-
 package updater
 
 func start(src string) string {