Files
2026-11-01 12:21:55 -05:00

174 lines
5.0 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"fmt"
"strings"
)
// AvailableUpdate describes a newer repository release for an installed tentacle.
type AvailableUpdate struct {
ID string
Name string
InstalledVersion string
AvailableVersion string
RepositoryURL string
RepositoryID string
LocalID string
}
// packageVersion accepts dotted numeric releases (including existing short
// versions such as 0.1), with optional SemVer prerelease and build identifiers.
type packageVersion struct {
numbers []string
prerelease []string
}
func numeric_identifier(value string) bool {
if value == "" {
return false
}
for _, ch := range value {
if ch < '0' || ch > '9' {
return false
}
}
return true
}
func version_identifiers(value string, prerelease bool) ([]string, error) {
identifiers := strings.Split(value, ".")
for _, identifier := range identifiers {
if identifier == "" {
return nil, fmt.Errorf("empty version identifier")
}
for _, ch := range identifier {
if !(ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch == '-') {
return nil, fmt.Errorf("invalid version identifier %q", identifier)
}
}
if prerelease && numeric_identifier(identifier) && len(identifier) > 1 && identifier[0] == '0' {
return nil, fmt.Errorf("prerelease number has a leading zero")
}
}
return identifiers, nil
}
func parse_package_version(value string) (packageVersion, error) {
var result packageVersion
core := strings.TrimPrefix(strings.TrimSpace(value), "v")
if before, build, found := strings.Cut(core, "+"); found {
if _, err := version_identifiers(build, false); err != nil {
return result, err
}
core = before
}
if before, prerelease, found := strings.Cut(core, "-"); found {
var err error
result.prerelease, err = version_identifiers(prerelease, true)
if err != nil {
return result, err
}
core = before
}
result.numbers = strings.Split(core, ".")
for _, number := range result.numbers {
if !numeric_identifier(number) {
return result, fmt.Errorf("invalid numeric version %q", value)
}
}
return result, nil
}
// Compare decimal strings without an integer-size limit.
func compare_version_number(first, second string) int {
first, second = strings.TrimLeft(first, "0"), strings.TrimLeft(second, "0")
if len(first) < len(second) {
return -1
}
if len(first) > len(second) {
return 1
}
return strings.Compare(first, second)
}
// IsNewerVersion compares release and prerelease precedence. Build metadata
// does not change precedence; missing numeric components are treated as zero.
func IsNewerVersion(available, installed string) (bool, error) {
first, err := parse_package_version(available)
if err != nil {
return false, err
}
second, err := parse_package_version(installed)
if err != nil {
return false, err
}
for index := 0; index < len(first.numbers) || index < len(second.numbers); index++ {
firstNumber, secondNumber := "0", "0"
if index < len(first.numbers) {
firstNumber = first.numbers[index]
}
if index < len(second.numbers) {
secondNumber = second.numbers[index]
}
if compared := compare_version_number(firstNumber, secondNumber); compared != 0 {
return compared > 0, nil
}
}
if len(first.prerelease) == 0 || len(second.prerelease) == 0 {
return len(first.prerelease) == 0 && len(second.prerelease) != 0, nil
}
for index := 0; index < len(first.prerelease) && index < len(second.prerelease); index++ {
firstID, secondID := first.prerelease[index], second.prerelease[index]
firstNumeric, secondNumeric := numeric_identifier(firstID), numeric_identifier(secondID)
compared := strings.Compare(firstID, secondID)
if firstNumeric && secondNumeric {
compared = compare_version_number(firstID, secondID)
}
if firstNumeric != secondNumeric {
return !firstNumeric, nil
}
if compared != 0 {
return compared > 0, nil
}
}
return len(first.prerelease) > len(second.prerelease), nil
}
// AvailableUpdates finds newer versions of installed repository tentacles.
func AvailableUpdates(snapshot Snapshot) []AvailableUpdate {
var updates []AvailableUpdate
for _, status := range snapshot.Entries {
entry := status.Entry
if entry.Repository == "" || entry.Version == "" {
continue
}
for _, repository := range snapshot.Repositories {
if repository.URL != entry.Repository || repository.ID != entry.RepositoryID {
continue
}
for _, manifest := range repository.Tentacles {
if manifest.ID != entry.LocalID {
continue
}
newer, err := IsNewerVersion(manifest.Version, entry.Version)
if err != nil || !newer {
continue
}
updates = append(updates, AvailableUpdate{
ID: entry.ID,
Name: manifest.Name,
InstalledVersion: entry.Version,
AvailableVersion: manifest.Version,
RepositoryURL: repository.URL,
RepositoryID: repository.ID,
LocalID: manifest.ID,
})
}
}
}
return updates
}