From 43c911c42d834264c6a3d03eeb5c21988d229c6a Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Fri, 14 Nov 2025 11:53:15 +0100 Subject: [PATCH 1/7] Add logic to preserve user partition --- flash.go | 10 +++++----- updater/flasher.go | 15 ++++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/flash.go b/flash.go index a439185..c0036c1 100644 --- a/flash.go +++ b/flash.go @@ -30,7 +30,7 @@ import ( ) func newFlashCmd() *cobra.Command { - var forceYes bool + var forceYes, preserveUser bool appCmd := &cobra.Command{ Use: "flash", Short: "Flash a Debian image on the board", @@ -66,11 +66,11 @@ NOTE: On Windows, required drivers are automatically installed with elevated pri Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { checkDriversInstalled() - runFlashCommand(cmd.Context(), args, forceYes) + runFlashCommand(cmd.Context(), args, forceYes, preserveUser) }, } appCmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "Automatically confirm all prompts") - // TODO: add --clean-install flag or something similar to distinguish between keeping and purging the /home directory + appCmd.Flags().BoolVar(&preserveUser, "preserve-user", false, "Preserve user partition") return appCmd } @@ -86,13 +86,13 @@ func checkDriversInstalled() { } } -func runFlashCommand(ctx context.Context, args []string, forceYes bool) { +func runFlashCommand(ctx context.Context, args []string, forceYes bool, preserveUser bool) { imagePath, err := paths.New(args[0]).Abs() if err != nil { feedback.Fatal(i18n.Tr("could not find image absolute path: %v", err), feedback.ErrBadArgument) } - err = updater.Flash(ctx, imagePath, args[0], forceYes) + err = updater.Flash(ctx, imagePath, args[0], forceYes, preserveUser) if err != nil { feedback.Fatal(i18n.Tr("error flashing the board: %v", err), feedback.ErrBadArgument) } diff --git a/updater/flasher.go b/updater/flasher.go index 9daae0e..0777888 100644 --- a/updater/flasher.go +++ b/updater/flasher.go @@ -28,7 +28,7 @@ import ( "github.com/arduino/arduino-flasher-cli/updater/artifacts" ) -func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool) error { +func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool) error { if !imagePath.Exist() { client := NewClient() @@ -89,10 +89,10 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes } yes := strings.ToLower(yesInput) == "yes" || strings.ToLower(yesInput) == "y" return yes, nil - }, forceYes) + }, forceYes, preserveUser) } -func FlashBoard(ctx context.Context, downloadedImagePath string, version string, upgradeConfirmCb DownloadConfirmCB, forceYes bool) error { +func FlashBoard(ctx context.Context, downloadedImagePath string, version string, upgradeConfirmCb DownloadConfirmCB, forceYes bool, preserveUser bool) error { if !forceYes { res, err := upgradeConfirmCb(version) if err != nil { @@ -139,9 +139,14 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, if err != nil { return err } - // TODO: add logic to preserve the user partition + + rawProgram := "rawprogram0.xml" + if preserveUser { + rawProgram = "rawprogram0.nouser.xml" + } + feedback.Print(i18n.Tr("Flashing with qdl")) - cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", "rawprogram0.xml", "patch0.xml") + cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", rawProgram, "patch0.xml") if err != nil { return err } From 24f234d603ffbc24dbae7b527c47ab6f6ecdaad2 Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Fri, 14 Nov 2025 14:18:48 +0100 Subject: [PATCH 2/7] Check partition table of the current board image --- updater/artifacts/artifacts_read_xml.go | 23 ++++++++++ updater/artifacts/read.xml | 4 ++ updater/flasher.go | 60 ++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 updater/artifacts/artifacts_read_xml.go create mode 100644 updater/artifacts/read.xml diff --git a/updater/artifacts/artifacts_read_xml.go b/updater/artifacts/artifacts_read_xml.go new file mode 100644 index 0000000..cf70f50 --- /dev/null +++ b/updater/artifacts/artifacts_read_xml.go @@ -0,0 +1,23 @@ +// This file is part of arduino-flasher-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-flasher-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package artifacts + +import ( + _ "embed" +) + +//go:embed read.xml +var ReadXML []byte diff --git a/updater/artifacts/read.xml b/updater/artifacts/read.xml new file mode 100644 index 0000000..95e37f3 --- /dev/null +++ b/updater/artifacts/read.xml @@ -0,0 +1,4 @@ + + + + diff --git a/updater/flasher.go b/updater/flasher.go index 0777888..7a9c9ad 100644 --- a/updater/flasher.go +++ b/updater/flasher.go @@ -17,6 +17,7 @@ package updater import ( "context" + "encoding/hex" "fmt" "runtime" "strings" @@ -93,7 +94,7 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes } func FlashBoard(ctx context.Context, downloadedImagePath string, version string, upgradeConfirmCb DownloadConfirmCB, forceYes bool, preserveUser bool) error { - if !forceYes { + if !forceYes && !preserveUser { res, err := upgradeConfirmCb(version) if err != nil { return err @@ -142,7 +143,30 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, rawProgram := "rawprogram0.xml" if preserveUser { - rawProgram = "rawprogram0.nouser.xml" + if ok, errT := checkBoardGPTTable(ctx, qdlPath, flashDir); ok && errT == nil { + rawProgram = "rawprogram0.nouser.xml" + } else { + res, err := func(target string) (bool, error) { + feedback.Printf("\nWARNING: %v.\nFlashing a new Linux image on the board will erase any existing data you have on it.", errT) + feedback.Printf("Do you want to proceed and flash %s on the board? (yes/no)", target) + + var yesInput string + _, err := fmt.Scanf("%s\n", &yesInput) + if err != nil { + return false, err + } + yes := strings.ToLower(yesInput) == "yes" || strings.ToLower(yesInput) == "y" + return yes, nil + }(version) + if err != nil { + return err + } + if !res { + feedback.Print(i18n.Tr("Flashing not confirmed by user, exiting")) + return nil + } + } + } feedback.Print(i18n.Tr("Flashing with qdl")) @@ -162,3 +186,35 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, return nil } + +func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) (bool, error) { + dumpBinPath := qdlPath.Parent().Join("dump.bin") + readXMLPath := qdlPath.Parent().Join("read.xml") + err := readXMLPath.WriteFile(artifacts.ReadXML) + if err != nil { + return false, err + } + cmd, err := paths.NewProcess(nil, qdlPath.String(), "--storage", "emmc", flashDir.Join("prog_firehose_ddr.elf").String(), readXMLPath.String()) + if err != nil { + return false, err + } + cmd.SetDir(qdlPath.Parent().String()) + if err := cmd.RunWithinContext(ctx); err != nil { + return false, err + } + if !dumpBinPath.Exist() { + return false, fmt.Errorf("it was not possible to access the current Debian image GPT table") + } + dump, err := dumpBinPath.ReadFile() + if err != nil { + return false, err + } + strDump := hex.Dump(dump) + + if strings.Contains(strDump, "00000250 4c 00 00 00") { + fmt.Println("R0") + return false, fmt.Errorf("the current Debian image (R0) does not support user partition preservation") + } + + return true, nil +} From 36e834578cdbd7be14cd09a94502cb75583a6f32 Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Mon, 17 Nov 2025 10:20:37 +0100 Subject: [PATCH 3/7] Check if the image to be flashed supports user partition preservation --- updater/flasher.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/updater/flasher.go b/updater/flasher.go index 7a9c9ad..8dfbba5 100644 --- a/updater/flasher.go +++ b/updater/flasher.go @@ -143,12 +143,16 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, rawProgram := "rawprogram0.xml" if preserveUser { - if ok, errT := checkBoardGPTTable(ctx, qdlPath, flashDir); ok && errT == nil { + if errT := checkBoardGPTTable(ctx, qdlPath, flashDir); errT == nil && flashDir.Join("rawprogram0.nouser.xml").Exist() { rawProgram = "rawprogram0.nouser.xml" } else { res, err := func(target string) (bool, error) { - feedback.Printf("\nWARNING: %v.\nFlashing a new Linux image on the board will erase any existing data you have on it.", errT) - feedback.Printf("Do you want to proceed and flash %s on the board? (yes/no)", target) + warnStr := "Linux image " + target + " does not support user partition preservation" + if errT != nil { + warnStr = errT.Error() + } + feedback.Printf("\nWARNING: %s.", warnStr) + feedback.Printf("Do you want to proceed and flash %s on the board, erasing any existing data you have on it.? (yes/no)", target) var yesInput string _, err := fmt.Scanf("%s\n", &yesInput) @@ -187,34 +191,33 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, return nil } -func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) (bool, error) { +func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) error { dumpBinPath := qdlPath.Parent().Join("dump.bin") readXMLPath := qdlPath.Parent().Join("read.xml") err := readXMLPath.WriteFile(artifacts.ReadXML) if err != nil { - return false, err + return err } cmd, err := paths.NewProcess(nil, qdlPath.String(), "--storage", "emmc", flashDir.Join("prog_firehose_ddr.elf").String(), readXMLPath.String()) if err != nil { - return false, err + return err } cmd.SetDir(qdlPath.Parent().String()) if err := cmd.RunWithinContext(ctx); err != nil { - return false, err + return err } if !dumpBinPath.Exist() { - return false, fmt.Errorf("it was not possible to access the current Debian image GPT table") + return fmt.Errorf("it was not possible to access the current Debian image GPT table") } dump, err := dumpBinPath.ReadFile() if err != nil { - return false, err + return err } strDump := hex.Dump(dump) if strings.Contains(strDump, "00000250 4c 00 00 00") { - fmt.Println("R0") - return false, fmt.Errorf("the current Debian image (R0) does not support user partition preservation") + return fmt.Errorf("the current Debian image (R0) does not support user partition preservation") } - return true, nil + return nil } From 404a0097c394f41d3716a9ce80dcbd68f3992c03 Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Tue, 18 Nov 2025 11:13:53 +0100 Subject: [PATCH 4/7] Run gofmt --- updater/artifacts/artifacts_read_xml.go | 46 ++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/updater/artifacts/artifacts_read_xml.go b/updater/artifacts/artifacts_read_xml.go index cf70f50..008a1e2 100644 --- a/updater/artifacts/artifacts_read_xml.go +++ b/updater/artifacts/artifacts_read_xml.go @@ -1,23 +1,23 @@ -// This file is part of arduino-flasher-cli. -// -// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) -// -// This software is released under the GNU General Public License version 3, -// which covers the main part of arduino-flasher-cli. -// The terms of this license can be found at: -// https://www.gnu.org/licenses/gpl-3.0.en.html -// -// You can be released from the requirements of the above licenses by purchasing -// a commercial license. Buying such a license is mandatory if you want to -// modify or otherwise use the software for commercial activities involving the -// Arduino software without disclosing the source code of your own applications. -// To purchase a commercial license, send an email to license@arduino.cc. - -package artifacts - -import ( - _ "embed" -) - -//go:embed read.xml -var ReadXML []byte +// This file is part of arduino-flasher-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-flasher-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package artifacts + +import ( + _ "embed" +) + +//go:embed read.xml +var ReadXML []byte From 5953691c0b81a2b2a393c172133218fb4fc52230 Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Tue, 18 Nov 2025 11:24:59 +0100 Subject: [PATCH 5/7] Appease linter --- updater/flasher.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/updater/flasher.go b/updater/flasher.go index 243c117..9ad94cb 100644 --- a/updater/flasher.go +++ b/updater/flasher.go @@ -33,6 +33,7 @@ import ( const GiB = uint64(1024 * 1024 * 1024) const DownloadDiskSpace = uint64(12) const ExtractDiskSpace = uint64(10) +const yesPrompt = "yes" func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool, tempDir string) error { if !imagePath.Exist() { @@ -62,7 +63,7 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes if err != nil { return false, err } - yes := strings.ToLower(yesInput) == "yes" || strings.ToLower(yesInput) == "y" + yes := strings.ToLower(yesInput) == yesPrompt || strings.ToLower(yesInput) == "y" return yes, nil }, forceYes, temp) @@ -117,7 +118,7 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes if err != nil { return false, err } - yes := strings.ToLower(yesInput) == "yes" || strings.ToLower(yesInput) == "y" + yes := strings.ToLower(yesInput) == yesPrompt || strings.ToLower(yesInput) == "y" return yes, nil }, forceYes, preserveUser) } @@ -188,7 +189,7 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, if err != nil { return false, err } - yes := strings.ToLower(yesInput) == "yes" || strings.ToLower(yesInput) == "y" + yes := strings.ToLower(yesInput) == yesPrompt || strings.ToLower(yesInput) == "y" return yes, nil }(version) if err != nil { From 0dee88945e220ee79197d029761de0e5f5163e94 Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Thu, 20 Nov 2025 14:59:24 +0100 Subject: [PATCH 6/7] Count the number of partitions to determine if it is a R0 image --- updater/artifacts/read.xml | 2 +- updater/flasher.go | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/updater/artifacts/read.xml b/updater/artifacts/read.xml index 95e37f3..fdc858a 100644 --- a/updater/artifacts/read.xml +++ b/updater/artifacts/read.xml @@ -1,4 +1,4 @@ - + diff --git a/updater/flasher.go b/updater/flasher.go index 9ad94cb..c35d379 100644 --- a/updater/flasher.go +++ b/updater/flasher.go @@ -20,6 +20,7 @@ import ( "encoding/hex" "fmt" "runtime" + "strconv" "strings" "github.com/arduino/go-paths-helper" @@ -245,7 +246,25 @@ func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) erro } strDump := hex.Dump(dump) - if strings.Contains(strDump, "00000250 4c 00 00 00") { + strDumpSlice := strings.Split(strDump, "\n") + // the max number of partitions is stored at entry 0x50 + maxPartitions, err := strconv.ParseInt(strings.Split(strDumpSlice[5], " ")[2], 16, 16) + if err != nil { + return err + } + + numPartitions := 0 + // starting from entry 0x200, there is a new partition every 0x80 bytes + // TODO: check if the size of each partition is 80h or just assume it? + for i := 32; numPartitions < int(maxPartitions); i += 8 { + // partitions are made of non-zero bytes, if all 0s then there are no more entries + if strings.Contains(strDumpSlice[i], "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00") { + break + } + numPartitions++ + } + + if numPartitions == 73 && maxPartitions == 76 { return fmt.Errorf("the current Debian image (R0) does not support user partition preservation") } From 3bb3277c5e5f5e7e902d0fb4b5f62c3bfbd8c58a Mon Sep 17 00:00:00 2001 From: MatteoPologruto Date: Wed, 10 Dec 2025 17:07:35 +0100 Subject: [PATCH 7/7] Return an error if user does not confirm flashing --- internal/updater/flasher.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/updater/flasher.go b/internal/updater/flasher.go index 6b84a31..5f3c60f 100644 --- a/internal/updater/flasher.go +++ b/internal/updater/flasher.go @@ -155,8 +155,7 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, return err } if !res { - feedback.Print(i18n.Tr("Flashing not confirmed by user, exiting")) - return nil + return fmt.Errorf("flashing not confirmed by user, exiting") } }