diff --git a/bpm_package/bpm.bpm b/bpm_package/bpm.bpm deleted file mode 100644 index b20728a..0000000 Binary files a/bpm_package/bpm.bpm and /dev/null differ diff --git a/bpm_package/files/usr/bin/bpm b/bpm_package/files/usr/bin/bpm deleted file mode 100755 index 4b0ecef..0000000 Binary files a/bpm_package/files/usr/bin/bpm and /dev/null differ diff --git a/utils.go b/bpm_utils/general_utils.go similarity index 61% rename from utils.go rename to bpm_utils/general_utils.go index fc1a6b5..321b806 100644 --- a/utils.go +++ b/bpm_utils/general_utils.go @@ -1,4 +1,4 @@ -package main +package bpm_utils import "syscall" @@ -20,6 +20,25 @@ func getKernel() string { return byteArrayToString(u.Sysname[:]) + " " + byteArrayToString(u.Release[:]) } +func stringSliceRemove(s []string, r string) []string { + for i, v := range s { + if v == r { + return append(s[:i], s[i+1:]...) + } + } + return s +} + +func stringSliceRemoveEmpty(s []string) []string { + var r []string + for _, str := range s { + if str != "" { + r = append(r, str) + } + } + return r +} + func byteArrayToString(bs []int8) string { b := make([]byte, len(bs)) for i, v := range bs { diff --git a/package_utils.go b/bpm_utils/package_utils.go similarity index 56% rename from package_utils.go rename to bpm_utils/package_utils.go index 08f3119..8d610cf 100644 --- a/package_utils.go +++ b/bpm_utils/package_utils.go @@ -1,4 +1,4 @@ -package main +package bpm_utils import ( "archive/tar" @@ -14,16 +14,16 @@ import ( "strings" ) -type packageInfo struct { - name string - description string - version string - pkgType string - depends []string - provides []string +type PackageInfo struct { + Name string + Description string + Version string + Type string + Depends []string + Provides []string } -func readPackage(filename string) (*packageInfo, error) { +func ReadPackage(filename string) (*PackageInfo, error) { if _, err := os.Stat(filename); os.IsNotExist(err) { return nil, err } @@ -50,7 +50,7 @@ func readPackage(filename string) (*packageInfo, error) { if err != nil { return nil, err } - pkgInfo, err := readPackageInfo(string(bs)) + pkgInfo, err := ReadPackageInfo(string(bs)) if err != nil { return nil, err } @@ -60,8 +60,8 @@ func readPackage(filename string) (*packageInfo, error) { return nil, errors.New("pkg.info not found in archive") } -func readPackageInfo(contents string) (*packageInfo, error) { - pkgInfo := packageInfo{} +func ReadPackageInfo(contents string) (*PackageInfo, error) { + pkgInfo := PackageInfo{} lines := strings.Split(contents, "\n") for num, line := range lines { if len(strings.TrimSpace(line)) == 0 { @@ -75,28 +75,40 @@ func readPackageInfo(contents string) (*packageInfo, error) { split[1] = strings.Trim(split[1], " ") switch split[0] { case "name": - pkgInfo.name = split[1] + pkgInfo.Name = split[1] case "description": - pkgInfo.description = split[1] + pkgInfo.Description = split[1] case "version": - pkgInfo.version = split[1] + pkgInfo.Version = split[1] case "type": - pkgInfo.pkgType = split[1] + pkgInfo.Type = split[1] + case "depends": + pkgInfo.Depends = strings.Split(strings.Replace(split[1], " ", "", -1), ",") + pkgInfo.Depends = stringSliceRemoveEmpty(pkgInfo.Depends) + case "provides": + pkgInfo.Provides = strings.Split(strings.Replace(split[1], " ", "", -1), ",") + pkgInfo.Provides = stringSliceRemoveEmpty(pkgInfo.Depends) } } return &pkgInfo, nil } -func createInfoFile(pkgInfo packageInfo) string { +func CreateInfoFile(pkgInfo PackageInfo) string { ret := "" - ret = ret + "name: " + pkgInfo.name + "\n" - ret = ret + "description: " + pkgInfo.description + "\n" - ret = ret + "version: " + pkgInfo.version + "\n" - ret = ret + "type: " + pkgInfo.pkgType + "\n" + ret = ret + "name: " + pkgInfo.Name + "\n" + ret = ret + "description: " + pkgInfo.Description + "\n" + ret = ret + "version: " + pkgInfo.Version + "\n" + ret = ret + "type: " + pkgInfo.Type + "\n" + if len(pkgInfo.Depends) > 0 { + ret = ret + "depends (" + strconv.Itoa(len(pkgInfo.Depends)) + "): " + strings.Join(pkgInfo.Depends, ",") + "\n" + } + if len(pkgInfo.Provides) > 0 { + ret = ret + "provides (" + strconv.Itoa(len(pkgInfo.Provides)) + "): " + strings.Join(pkgInfo.Provides, ",") + "\n" + } return ret } -func installPackage(filename, installDir string) error { +func InstallPackage(filename, installDir string, force bool) error { if _, err := os.Stat(filename); os.IsNotExist(err) { return err } @@ -110,7 +122,15 @@ func installPackage(filename, installDir string) error { } tr := tar.NewReader(archive) var files []string - var pkgInfo *packageInfo + pkgInfo, err := ReadPackage(filename) + if err != nil { + return err + } + if !force { + if unresolved := CheckDependencies(pkgInfo, installDir); len(unresolved) != 0 { + return errors.New("Could not resolve all dependencies. Missing " + strings.Join(unresolved, ", ")) + } + } for { header, err := tr.Next() if err == io.EOF { @@ -119,16 +139,6 @@ func installPackage(filename, installDir string) error { if err != nil { return err } - if header.Name == "pkg.info" { - bs, _ := io.ReadAll(tr) - if err != nil { - return err - } - pkgInfo, err = readPackageInfo(string(bs)) - if err != nil { - return err - } - } if strings.HasPrefix(header.Name, "files/") && header.Name != "files/" { extractFilename := path.Join(installDir, strings.TrimPrefix(header.Name, "files/")) switch header.Typeflag { @@ -164,12 +174,12 @@ func installPackage(filename, installDir string) error { slices.Sort(files) slices.Reverse(files) - dataDir := path.Join(installDir, "var/lib/bpm/installed/") - err = os.MkdirAll(dataDir, 755) + installedDir := path.Join(installDir, "var/lib/bpm/installed/") + err = os.MkdirAll(installedDir, 755) if err != nil { return err } - pkgDir := path.Join(dataDir, pkgInfo.name) + pkgDir := path.Join(installedDir, pkgInfo.Name) err = os.RemoveAll(pkgDir) if err != nil { return err @@ -189,35 +199,70 @@ func installPackage(filename, installDir string) error { return err } } - f.Close() + err = f.Close() + if err != nil { + return err + } f, err = os.Create(path.Join(pkgDir, "info")) if err != nil { return err } - _, err = f.WriteString(createInfoFile(*pkgInfo)) + _, err = f.WriteString(CreateInfoFile(*pkgInfo)) + if err != nil { + return err + } + err = f.Close() if err != nil { return err } - f.Close() - archive.Close() - file.Close() + err = archive.Close() + if err != nil { + return err + } + err = file.Close() + if err != nil { + return err + } return nil } -func isPackageInstalled(pkg string) bool { - dataDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(dataDir, pkg) +func CheckDependencies(pkgInfo *PackageInfo, rootDir string) []string { + unresolved := make([]string, len(pkgInfo.Depends)) + copy(unresolved, pkgInfo.Depends) + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + if _, err := os.Stat(installedDir); err != nil { + return nil + } + items, err := os.ReadDir(installedDir) + if err != nil { + return nil + } + + for _, item := range items { + if !item.IsDir() { + continue + } + if slices.Contains(unresolved, item.Name()) { + unresolved = stringSliceRemove(unresolved, item.Name()) + } + } + return unresolved +} + +func IsPackageInstalled(pkg, rootDir string) bool { + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + pkgDir := path.Join(installedDir, pkg) if _, err := os.Stat(pkgDir); err != nil { return false } return true } -func getInstalledPackages() ([]string, error) { - dataDir := path.Join(rootDir, "var/lib/bpm/installed/") - items, err := os.ReadDir(dataDir) +func GetInstalledPackages(rootDir string) ([]string, error) { + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + items, err := os.ReadDir(installedDir) if os.IsNotExist(err) { return nil, nil } @@ -231,12 +276,12 @@ func getInstalledPackages() ([]string, error) { return ret, nil } -func getPackageFiles(pkg string) []string { +func GetPackageFiles(pkg, rootDir string) []string { var ret []string - dataDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(dataDir, pkg) + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + pkgDir := path.Join(installedDir, pkg) files := path.Join(pkgDir, "files") - if _, err := os.Stat(dataDir); os.IsNotExist(err) { + if _, err := os.Stat(installedDir); os.IsNotExist(err) { return nil } if _, err := os.Stat(pkgDir); os.IsNotExist(err) { @@ -253,11 +298,11 @@ func getPackageFiles(pkg string) []string { return ret } -func getPackageInfo(pkg string) *packageInfo { - dataDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(dataDir, pkg) +func GetPackageInfo(pkg, rootDir string) *PackageInfo { + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + pkgDir := path.Join(installedDir, pkg) files := path.Join(pkgDir, "info") - if _, err := os.Stat(dataDir); os.IsNotExist(err) { + if _, err := os.Stat(installedDir); os.IsNotExist(err) { return nil } if _, err := os.Stat(pkgDir); os.IsNotExist(err) { @@ -271,17 +316,17 @@ func getPackageInfo(pkg string) *packageInfo { if err != nil { return nil } - info, err := readPackageInfo(string(bs)) + info, err := ReadPackageInfo(string(bs)) if err != nil { return nil } return info } -func removePackage(pkg string) error { - dataDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(dataDir, pkg) - files := getPackageFiles(pkg) +func RemovePackage(pkg, rootDir string) error { + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + pkgDir := path.Join(installedDir, pkg) + files := GetPackageFiles(pkg, rootDir) for _, file := range files { file = path.Join(rootDir, file) stat, err := os.Stat(file) diff --git a/go.mod b/go.mod index 4563329..5288e56 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module bpm +module capcreepergr.me/bpm go 1.22 diff --git a/main.go b/main.go index dcce01c..e40625d 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "capcreepergr.me/bpm/bpm_utils" "fmt" "log" "os" @@ -14,11 +15,14 @@ import ( /* A simple-to-use package manager */ /* ---------------------------------- */ -var bpmVer = "0.0.3" -var rootDir string = "/" +var bpmVer = "0.0.5" +var rootDir = "/" func main() { - //fmt.Printf("Running %s %s\n", getKernel(), getArch()) + if os.Getuid() != 0 { + fmt.Println("BPM needs to be run with superuser permissions") + os.Exit(0) + } resolveCommand() } @@ -76,19 +80,19 @@ func resolveCommand() { return } for n, pkg := range packages { - info := getPackageInfo(pkg) + info := bpm_utils.GetPackageInfo(pkg, rootDir) if info == nil { fmt.Printf("Package (%s) could not be found\n", pkg) continue } - fmt.Print("----------------\n" + createInfoFile(*info)) + fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*info)) if n == len(packages)-1 { fmt.Println("----------------") } } case list: resolveFlags() - packages, err := getInstalledPackages() + packages, err := bpm_utils.GetInstalledPackages(rootDir) if err != nil { log.Fatalf("Could not get installed packages\nError: %s", err.Error()) return @@ -98,12 +102,12 @@ func resolveCommand() { return } for n, pkg := range packages { - info := getPackageInfo(pkg) + info := bpm_utils.GetPackageInfo(pkg, rootDir) if info == nil { fmt.Printf("Package (%s) could not be found\n", pkg) continue } - fmt.Print("----------------\n" + createInfoFile(*info)) + fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*info)) if n == len(packages)-1 { fmt.Println("----------------") } @@ -116,33 +120,38 @@ func resolveCommand() { return } for _, file := range files { - pkgInfo, err := readPackage(file) + pkgInfo, err := bpm_utils.ReadPackage(file) if err != nil { log.Fatalf("Could not read package\nError: %s\n", err) } - fmt.Print("----------------\n" + createInfoFile(*pkgInfo)) + fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*pkgInfo)) fmt.Println("----------------") - if isPackageInstalled(pkgInfo.name) { + if !slices.Contains(flags, "f") { + if unresolved := bpm_utils.CheckDependencies(pkgInfo, rootDir); len(unresolved) != 0 { + log.Fatalf("Cannot install package (%s) due to missing dependencies: %s\n", pkgInfo.Name, strings.Join(unresolved, ", ")) + } + } + if bpm_utils.IsPackageInstalled(pkgInfo.Name, rootDir) { if !slices.Contains(flags, "y") { - installedInfo := getPackageInfo(pkgInfo.name) - if strings.Compare(pkgInfo.version, installedInfo.version) > 0 { - fmt.Println("This file contains a newer version of this package (" + installedInfo.version + " -> " + pkgInfo.version + ")") + installedInfo := bpm_utils.GetPackageInfo(pkgInfo.Name, rootDir) + if strings.Compare(pkgInfo.Version, installedInfo.Version) > 0 { + fmt.Println("This file contains a newer version of this package (" + installedInfo.Version + " -> " + pkgInfo.Version + ")") fmt.Print("Do you wish to update this package? [y\\N] ") - } else if strings.Compare(pkgInfo.version, installedInfo.version) < 0 { - fmt.Println("This file contains an older version of this package (" + installedInfo.version + " -> " + pkgInfo.version + ")") + } else if strings.Compare(pkgInfo.Version, installedInfo.Version) < 0 { + fmt.Println("This file contains an older version of this package (" + installedInfo.Version + " -> " + pkgInfo.Version + ")") fmt.Print("Do you wish to downgrade this package? (Not recommended) [y\\N] ") - } else if strings.Compare(pkgInfo.version, installedInfo.version) == 0 { + } else if strings.Compare(pkgInfo.Version, installedInfo.Version) == 0 { fmt.Println("This package is already installed on the system and is up to date") fmt.Print("Do you wish to reinstall this package? [y\\N] ") } reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { - fmt.Printf("Skipping package (%s)...\n", pkgInfo.name) + fmt.Printf("Skipping package (%s)...\n", pkgInfo.Name) continue } } - err := removePackage(pkgInfo.name) + err := bpm_utils.RemovePackage(pkgInfo.Name, rootDir) if err != nil { log.Fatalf("Could not remove current version of the package\nError: %s\n", err) } @@ -151,16 +160,16 @@ func resolveCommand() { reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { - fmt.Printf("Skipping package (%s)...\n", pkgInfo.name) + fmt.Printf("Skipping package (%s)...\n", pkgInfo.Name) continue } } - err = installPackage(file, rootDir) + err = bpm_utils.InstallPackage(file, rootDir, slices.Contains(flags, "f")) if err != nil { log.Fatalf("Could not install package\nError: %s\n", err) } - fmt.Printf("Package (%s) was successfully installed!\n", pkgInfo.name) + fmt.Printf("Package (%s) was successfully installed!\n", pkgInfo.Name) } case remove: flags, i := resolveFlags() @@ -170,37 +179,41 @@ func resolveCommand() { return } for _, pkg := range packages { - pkgInfo := getPackageInfo(pkg) + pkgInfo := bpm_utils.GetPackageInfo(pkg, rootDir) if pkgInfo == nil { fmt.Printf("Package (%s) could not be found\n", pkg) continue } - fmt.Print("----------------\n" + createInfoFile(*pkgInfo)) + fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*pkgInfo)) fmt.Println("----------------") if !slices.Contains(flags, "y") { reader := bufio.NewReader(os.Stdin) fmt.Print("Do you wish to remove this package? [y\\N] ") text, _ := reader.ReadString('\n') if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { - fmt.Printf("Skipping package (%s)...\n", pkgInfo.name) + fmt.Printf("Skipping package (%s)...\n", pkgInfo.Name) continue } } - err := removePackage(pkg) + err := bpm_utils.RemovePackage(pkg, rootDir) if err != nil { log.Fatalf("Could not remove package\nError: %s\n", err) } - fmt.Printf("Package (%s) was successfully removed!\n", pkgInfo.name) + fmt.Printf("Package (%s) was successfully removed!\n", pkgInfo.Name) } default: - fmt.Println("------Help------") - fmt.Println("bpm version | shows information on the installed version of bpm") - fmt.Println("bpm info | shows information on an installed package") - fmt.Println("bpm list [-e] | lists all installed packages. -e lists only explicitly installed packages") - fmt.Println("bpm install [-y] | installs the following files. -y skips the confirmation prompt") - fmt.Println("bpm remove [-y] | removes the following packages. -y skips the confirmation prompt") - fmt.Println("bpm cleanup | removes all unneeded dependencies") - fmt.Println("----------------") + fmt.Println("\033[1m------Help------\033[0m") + fmt.Println("\033[1m\\ Command Format /\033[0m") + fmt.Println("-> command format: bpm [-flags]...") + fmt.Println("-> flags will be read if passed right after the subcommand otherwise they will be read as subcommand arguments") + fmt.Println("\033[1m\\ Command List /\033[0m") + fmt.Println("-> bpm version | shows information on the installed version of bpm") + fmt.Println("-> bpm info | shows information on an installed package") + fmt.Println("-> bpm list | lists all installed packages") + fmt.Println("-> bpm install [-y, -f] | installs the following files. -y skips the confirmation prompt. -f skips dependency resolution") + fmt.Println("-> bpm remove [-y] | removes the following packages. -y skips the confirmation prompt") + fmt.Println("-> bpm cleanup | removes all unneeded dependencies") + fmt.Println("\033[1m----------------\033[0m") } } @@ -213,14 +226,8 @@ func resolveFlags() ([]string, int) { switch getCommandType() { default: log.Fatalf("Invalid flag " + flag) - case list: - v := [...]string{"e"} - if !slices.Contains(v[:], f) { - log.Fatalf("Invalid flag " + flag) - } - ret = append(ret, f) case install: - v := [...]string{"y"} + v := [...]string{"y", "f"} if !slices.Contains(v[:], f) { log.Fatalf("Invalid flag " + flag) } diff --git a/test_hello_package/hello.bpm b/test_hello_package/hello.bpm deleted file mode 100644 index 94bd298..0000000 Binary files a/test_hello_package/hello.bpm and /dev/null differ diff --git a/test_packages/bpm-utils/bpm-utils.bpm b/test_packages/bpm-utils/bpm-utils.bpm new file mode 100644 index 0000000..f273a85 Binary files /dev/null and b/test_packages/bpm-utils/bpm-utils.bpm differ diff --git a/test_packages/bpm-utils/files/usr/bin/bpm-create b/test_packages/bpm-utils/files/usr/bin/bpm-create new file mode 100755 index 0000000..b9fd3e8 --- /dev/null +++ b/test_packages/bpm-utils/files/usr/bin/bpm-create @@ -0,0 +1,28 @@ +#!/bin/bash +if [ $# -eq 0 ] +then + echo "No output package name given!" + exit 1 +fi + +output=$1 + +echo "Creating package with the name $output..." + +if [ -d files ]; then + echo "files/ directory found" +else + echo "files/ directory not found in $PWD" + exit 1 +fi + +if [ -f pkg.info ]; then + echo "pkg.info file found" +else + echo "pkg.info file not found in $PWD" + exit 1 +fi + +echo "Creating $output.bpm package..." + +tar -czf $output.bpm files/ pkg.info diff --git a/test_packages/bpm-utils/pkg.info b/test_packages/bpm-utils/pkg.info new file mode 100644 index 0000000..1a206eb --- /dev/null +++ b/test_packages/bpm-utils/pkg.info @@ -0,0 +1,4 @@ +name: bpm-utils +description: Utilities to create BPM packages +version: 1.0.0 +type: binary \ No newline at end of file diff --git a/test_packages/bpm/bpm.bpm b/test_packages/bpm/bpm.bpm new file mode 100644 index 0000000..d934eb2 Binary files /dev/null and b/test_packages/bpm/bpm.bpm differ diff --git a/test_packages/bpm/files/usr/bin/bpm b/test_packages/bpm/files/usr/bin/bpm new file mode 100755 index 0000000..ceb46e6 Binary files /dev/null and b/test_packages/bpm/files/usr/bin/bpm differ diff --git a/bpm_package/pkg.info b/test_packages/bpm/pkg.info similarity index 80% rename from bpm_package/pkg.info rename to test_packages/bpm/pkg.info index 9617a31..b0803c5 100644 --- a/bpm_package/pkg.info +++ b/test_packages/bpm/pkg.info @@ -1,4 +1,4 @@ name: bpm description: The Bubble Package Manager -version: 0.0.3 +version: 0.0.5 type: binary diff --git a/test_hello_package/files/usr/bin/hello b/test_packages/hello/files/usr/bin/hello similarity index 100% rename from test_hello_package/files/usr/bin/hello rename to test_packages/hello/files/usr/bin/hello diff --git a/test_packages/hello/hello.bpm b/test_packages/hello/hello.bpm new file mode 100644 index 0000000..4989e18 Binary files /dev/null and b/test_packages/hello/hello.bpm differ diff --git a/test_hello_package/pkg.info b/test_packages/hello/pkg.info similarity index 81% rename from test_hello_package/pkg.info rename to test_packages/hello/pkg.info index de1bc65..34646ad 100644 --- a/test_hello_package/pkg.info +++ b/test_packages/hello/pkg.info @@ -1,4 +1,4 @@ name: hello description: A simple hello world program -version: 1.0-1 +version: 1.0 type: binary \ No newline at end of file diff --git a/version.go b/version.go deleted file mode 100644 index 6c5832f..0000000 --- a/version.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -// Package cmpver implements a variant of debian version number -// comparison. -// -// A version is a string consisting of alternating non-numeric and -// numeric fields. When comparing two versions, each one is broken -// down into its respective fields, and the fields are compared -// pairwise. The comparison is lexicographic for non-numeric fields, -// numeric for numeric fields. The first non-equal field pair -// determines the ordering of the two versions. -// -// This comparison scheme is a simplified version of Debian's version -// number comparisons. Debian differs in a few details of -// lexicographical field comparison, where certain characters have -// special meaning and ordering. We don't need that, because Tailscale -// version numbers don't need it. - -package main - -import ( - "fmt" - "strconv" - "strings" -) - -// Less reports whether v1 is less than v2. -// -// Note that "12" is less than "12.0". -func Less(v1, v2 string) bool { - return Compare(v1, v2) < 0 -} - -// LessEq reports whether v1 is less than or equal to v2. -// -// Note that "12" is less than "12.0". -func LessEq(v1, v2 string) bool { - return Compare(v1, v2) <= 0 -} - -func isnum(r rune) bool { - return r >= '0' && r <= '9' -} - -func notnum(r rune) bool { - return !isnum(r) -} - -// Compare returns an integer comparing two strings as version numbers. -// The result will be -1, 0, or 1 representing the sign of v1 - v2: -// -// Compare(v1, v2) < 0 if v1 < v2 -// == 0 if v1 == v2 -// > 0 if v1 > v2 -func Compare(v1, v2 string) int { - var ( - f1, f2 string - n1, n2 uint64 - err error - ) - for v1 != "" || v2 != "" { - // Compare the non-numeric character run lexicographically. - f1, v1 = splitPrefixFunc(v1, notnum) - f2, v2 = splitPrefixFunc(v2, notnum) - - if res := strings.Compare(f1, f2); res != 0 { - return res - } - - // Compare the numeric character run numerically. - f1, v1 = splitPrefixFunc(v1, isnum) - f2, v2 = splitPrefixFunc(v2, isnum) - - // ParseUint refuses to parse empty strings, which would only - // happen if we reached end-of-string. We follow the Debian - // convention that empty strings mean zero, because - // empirically that produces reasonable-feeling comparison - // behavior. - n1 = 0 - if f1 != "" { - n1, err = strconv.ParseUint(f1, 10, 64) - if err != nil { - panic(fmt.Sprintf("all-number string %q didn't parse as string: %s", f1, err)) - } - } - - n2 = 0 - if f2 != "" { - n2, err = strconv.ParseUint(f2, 10, 64) - if err != nil { - panic(fmt.Sprintf("all-number string %q didn't parse as string: %s", f2, err)) - } - } - - switch { - case n1 == n2: - case n1 < n2: - return -1 - case n1 > n2: - return 1 - } - } - - // Only way to reach here is if v1 and v2 run out of fields - // simultaneously - i.e. exactly equal versions. - return 0 -} - -// splitPrefixFunc splits s at the first rune where f(rune) is false. -func splitPrefixFunc(s string, f func(rune) bool) (string, string) { - for i, r := range s { - if !f(r) { - return s[:i], s[i:] - } - } - return s, s[:0] -}